@engine-room/after-effects-mcp 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,32 +1,25 @@
1
- # Engine Room — After Effects for AI agents
1
+ # After Effects MCP
2
2
 
3
- Drive Adobe After Effects by describing what you want. Comps, layers, keyframes with real easing, expressions, effects, text, shapes, masks **60 tools**, driven by an agent that can read the project back before it changes anything.
3
+ **Control Adobe After Effects with AI.** Describe the animation you want — a lower third, a logo reveal, an animated counter and it gets built in your project: layers, keyframes, easing, effects, expressions and text, all editable afterwards like anything you would make by hand.
4
4
 
5
- Built for motion designers, not just developers. Setup happens through conversation; you never have to open a terminal if you don't want to.
5
+ Works with Claude and other MCP clients, on macOS and Windows.
6
6
 
7
- ```
8
- "Build a lower third that says Chapter One, sliding in from the left with an easy ease,
9
- in my house style."
10
- ```
7
+ Requires After Effects 2026 and [Node.js 20+](https://nodejs.org).
11
8
 
12
- **Requirements:** macOS · After Effects 2026 · [Node.js 20+](https://nodejs.org)
9
+ ## Getting started
13
10
 
14
- ---
11
+ ### 1. Create a project folder
15
12
 
16
- ## Install
13
+ In a terminal:
17
14
 
18
- ### Claude Code (recommended)
19
-
20
- ```
21
- /plugin marketplace add Engine-Room-Games/after-effects-mcp
22
- /plugin install after-effects@engine-room
15
+ ```bash
16
+ npx @engine-room/after-effects-mcp init my-video
17
+ cd my-video
23
18
  ```
24
19
 
25
- That installs the tools *and* the knowledge of how to use them well two skills covering AE craft and setup troubleshooting.
26
-
27
- ### Any other MCP client
20
+ Open that folder in your AI client. It contains everything the client needs to find the tools.
28
21
 
29
- Add this to your client's MCP config (for Claude Desktop, `~/Library/Application Support/Claude/claude_desktop_config.json`):
22
+ *If your client uses one global configuration file rather than per-folder settings* — Claude Desktop, for example — add this to it instead. On macOS it is `~/Library/Application Support/Claude/claude_desktop_config.json`:
30
23
 
31
24
  ```json
32
25
  {
@@ -39,110 +32,105 @@ Add this to your client's MCP config (for Claude Desktop, `~/Library/Application
39
32
  }
40
33
  ```
41
34
 
42
- Restart the client.
35
+ Restart the client afterwards.
43
36
 
44
- ## Connect After Effects
37
+ ### 2. Connect After Effects
45
38
 
46
- The tools talk to a small panel that runs **inside** After Effects. Installing it is a conversation, not a chore:
39
+ Open After Effects, then ask the AI:
47
40
 
48
- > **You:** Set up After Effects.
41
+ > Set up After Effects.
49
42
 
50
- Claude runs `check_setup`, tells you what's missing, asks before it changes anything, then runs `setup_panel` which installs the panel and switches on the Adobe preference that allows it to load. Then:
43
+ It installs a small panel inside After Effects, then tells you to quit and reopen it. Ask it to check the setup again to confirm.
51
44
 
52
- 1. **Quit and reopen After Effects.** The panel only loads at launch.
53
- 2. If it still doesn't connect, **reboot once**. On some macOS builds the Adobe preference only applies after a restart. One time only.
45
+ This is a one-time step per machine. Until it is done, every tool reports that it cannot reach After Effects.
54
46
 
55
- Ask Claude to "check the After Effects setup" any time something stops working.
47
+ ### 3. Start working
56
48
 
57
- ## Start a project
49
+ With After Effects open and your project folder open in your client, describe what you want:
58
50
 
59
- ```bash
60
- npx @engine-room/after-effects-mcp init my-video
61
- ```
51
+ > Build a lower third that says Chapter One, sliding in from the left.
62
52
 
63
- This creates a folder built around one idea **the plugin knows the tool, you own the taste**:
53
+ The AI reads the current state of your comp, makes the change, and can take a screenshot to check the result.
64
54
 
65
- ```
66
- my-video/
67
- ├── .claude/skills/house-style/SKILL.md ← your palette, type, timing. Edit this.
68
- ├── CLAUDE.md ← what this project is
69
- └── renders/
70
- ```
55
+ Day to day, this is the only step you repeat. Create a new folder when you start a new project.
71
56
 
72
- Fill in `house-style/SKILL.md` with your colours, fonts and motion defaults and everything Claude builds follows them. Upgrading the plugin never touches it; your style survives tool updates, and the tool's knowledge updates without touching your style.
57
+ ## Your house style
73
58
 
74
- The fastest way to write it: build one piece the way you like it, then ask *"read this comp and write it up in my house-style skill."*
59
+ Each project folder contains `.claude/skills/house-style/SKILL.md`. Fill it in with your colours, fonts and motion defaults, and everything built afterwards follows them.
75
60
 
76
- Keep a folder per client or series. Or put the file in `~/.claude/skills/house-style/` to make it your default everywhere.
61
+ A quick way to write it: build one piece the way you want it, then ask the AI to read that comp and write it up in the house-style skill.
77
62
 
78
- > Didn't install the plugin? Run `init` with `--with-mcp` to add a project-level connection instead.
63
+ Keep one folder per client or series. To apply the same style everywhere instead, put the skill in `~/.claude/skills/house-style/`.
79
64
 
80
- ---
65
+ Updating the tools never touches this file.
81
66
 
82
- ## What it can do
67
+ ## Updating
83
68
 
84
- | Group | Tools |
85
- |---|---|
86
- | **Comps** (7) | `list_comps`, `get_comp`, `get_comp_tree`, `create_comp`, `set_comp`, `delete_comp`, `set_active_comp` |
87
- | **Layers** (15) | `list_layers`, `get_layer_full` ⭐, `create_{text,shape,solid,null,adjustment,precomp,camera,light}_layer`, `duplicate_layer`, `delete_layer`, `set_layer`, `parent_layer`, `reorder_layer` |
88
- | **Transforms** (1) | `set_transform` — position/scale/rotation/anchor/opacity, 2D and 3D, optionally keyframed |
89
- | **Keyframes** (6) | `add_keyframe`, `remove_keyframe`, `get_keyframes`, `set_interpolation`, `set_temporal_ease`, `set_spatial_tangents` |
90
- | **Expressions** (4) | `get_expression`, `set_expression`, `toggle_expression`, `clear_expression` |
91
- | **Effects** (6) | `list_effects`, `add_effect`, `remove_effect`, `set_effect_param`, `set_effect_enabled`, `list_available_effects` |
92
- | **Text** (2) | `set_text`, `add_text_animator` |
93
- | **Shapes** (3) | `set_shape_path`, `add_shape_content`, `set_shape_property` |
94
- | **Masks** (3) | `add_mask`, `set_mask`, `remove_mask` |
95
- | **Markers** (2) | `add_marker`, `remove_marker` |
96
- | **Vision** (2) | `screenshot_frame`, `screenshot_layer` |
97
- | **Batch** (1) | `run_batch` — many ops, one undo step, progress streaming |
98
- | **Explore** (2) | `get_project_summary`, `find_layers` |
99
- | **Raw** (1) | `run_jsx` — arbitrary ExtendScript escape hatch |
100
- | **Jobs** (3) | `await_job`, `get_job`, `cancel_job` |
101
- | **Setup** (2) | `check_setup`, `setup_panel` |
102
-
103
- ⭐ **`get_layer_full` is the centrepiece.** One call returns a layer's transforms *with* keyframes and expressions, every effect with every parameter, masks, markers and visible bounds. Reading before writing is what separates an agent that builds what you asked for from one that guesses.
104
-
105
- Three design decisions worth knowing about:
106
-
107
- - **Screenshots are diagnostics, not a feedback loop.** The tool descriptions tell the agent to take two or three, never to scrub frame by frame. Pass `downsample` on large comps — a full 4K frame is big enough to exhaust an agent's context in one call.
108
- - **Bulk work is one undo step.** `run_batch` runs hundreds of operations in a single ExtendScript pass, so "undo that" does what you mean. Long batches stream progress.
109
- - **Failures are loud.** Tools that can't do what was asked return an error naming the problem rather than reporting success — an agent can only correct a mistake it's told about.
110
-
111
- ## How it works
69
+ Two things to update: the tools, and the panel inside After Effects. The panel does not update on its own.
112
70
 
113
- ```
114
- Claude / MCP client
115
- │ stdio (JSON-RPC)
116
-
117
- MCP server (Node)
118
- │ HTTP + WebSocket on 127.0.0.1
119
-
120
- CEP panel inside After Effects
121
- │ evalScript
122
-
123
- ExtendScript → After Effects
124
- ```
71
+ Restart your AI client — it picks up the current version of the tools. Then, with After Effects open, ask:
72
+
73
+ > Update the After Effects panel.
74
+
75
+ It reinstalls the panel and tells you to restart After Effects. Ask it to check the setup afterwards, which reports whether the panel matches the current version.
125
76
 
126
- The panel is the only thing that talks to AE, and it serialises calls because ExtendScript is single-threaded. The server is stateless apart from a job table. The bridge listens on localhost only and refuses outside connections.
77
+ ## Tools
78
+
79
+ | Group | Tools |
80
+ |---|---|
81
+ | Comps (7) | `list_comps`, `get_comp`, `get_comp_tree`, `create_comp`, `set_comp`, `delete_comp`, `set_active_comp` |
82
+ | Layers (15) | `list_layers`, `get_layer_full`, `create_{text,shape,solid,null,adjustment,precomp,camera,light}_layer`, `duplicate_layer`, `delete_layer`, `set_layer`, `parent_layer`, `reorder_layer` |
83
+ | Transforms (1) | `set_transform` — position, scale, rotation, anchor, opacity; 2D and 3D; optionally keyframed |
84
+ | Keyframes (6) | `add_keyframe`, `remove_keyframe`, `get_keyframes`, `set_interpolation`, `set_temporal_ease`, `set_spatial_tangents` |
85
+ | Expressions (4) | `get_expression`, `set_expression`, `toggle_expression`, `clear_expression` |
86
+ | Effects (6) | `list_effects`, `add_effect`, `remove_effect`, `set_effect_param`, `set_effect_enabled`, `list_available_effects` |
87
+ | Text (2) | `set_text`, `add_text_animator` |
88
+ | Shapes (3) | `set_shape_path`, `add_shape_content`, `set_shape_property` |
89
+ | Masks (3) | `add_mask`, `set_mask`, `remove_mask` |
90
+ | Markers (2) | `add_marker`, `remove_marker` |
91
+ | Vision (2) | `screenshot_frame`, `screenshot_layer` |
92
+ | Batch (1) | `run_batch` |
93
+ | Explore (2) | `get_project_summary`, `find_layers` |
94
+ | Raw (1) | `run_jsx` |
95
+ | Jobs (3) | `await_job`, `get_job`, `cancel_job` |
96
+ | Setup (2) | `check_setup`, `setup_panel` |
97
+
98
+ A few notes on the ones that behave differently from the rest:
99
+
100
+ - `get_layer_full` returns a layer's transforms with their keyframes and expressions, every effect with every parameter, masks, markers and visible bounds — in one call.
101
+ - `run_batch` runs many operations in a single pass and counts as one undo step. Long batches stream progress.
102
+ - `screenshot_frame` and `screenshot_layer` are for occasional checks, not for reviewing motion frame by frame. On large comps, `downsample: 2` renders at half resolution, which is faster and keeps the image small.
103
+ - `run_jsx` runs arbitrary ExtendScript for anything the other tools do not cover.
127
104
 
128
105
  ## Troubleshooting
129
106
 
130
- Ask Claude to run `check_setup` first it reports exactly which link in the chain is broken and what to do about it.
107
+ Ask the AI to check the After Effects setup. It reports which part is broken and what to do about it.
131
108
 
132
- | Symptom | Cause |
109
+ | Symptom | Cause and fix |
133
110
  |---|---|
134
- | "Cannot reach the After Effects panel" | AE isn't running, or the panel isn't installed. Run `setup_panel`, then restart AE. |
135
- | Tools worked before, now error inside AE | The server was upgraded but the panel wasn't. Run `setup_panel`, restart AE. |
136
- | Panel never loads, setup looks correct | Reboot the Mac once the Adobe preference sometimes needs it. |
137
- | Want to see the panel's own log | In AE: **Window > Extensions > AE MCP Bridge**. |
111
+ | "Cannot reach the After Effects panel" | AE is not running, or the panel is not installed. Ask the AI to set up After Effects, then restart AE. |
112
+ | Tools worked before, now fail inside AE | The tools were updated but the panel was not. Ask the AI to update the panel, then restart AE. |
113
+ | Panel never loads, setup looks correct | On macOS, reboot once. Some builds cache the Adobe setting until a restart. |
114
+ | A panel answers but the setup check reports none installed | An older install is still serving. Remove it from the CEP extensions folder, reinstall the panel, restart AE. |
115
+ | Need the panel's own log | In AE: Window → Extensions → AE MCP Bridge. |
116
+
117
+ ## Platforms
118
+
119
+ macOS and Windows. Two things differ, and are handled for you:
120
+
121
+ | | macOS | Windows |
122
+ |---|---|---|
123
+ | Panel location | `~/Library/Application Support/Adobe/CEP/extensions` | `%APPDATA%\Adobe\CEP\extensions` |
124
+ | Unsigned-panel setting | `defaults` preference | `HKCU\Software\Adobe\CSXS.*` registry value |
125
+
126
+ macOS is the more exercised of the two; [issue reports](https://github.com/Engine-Room-Games/after-effects-mcp/issues) are welcome.
138
127
 
139
128
  ## Limitations
140
129
 
141
- - **macOS only.** Windows uses a different CEP location and a registry key; not implemented.
142
- - **Unsigned panel.** Loading it requires Adobe's `PlayerDebugMode`, which `setup_panel` enables. This is Adobe's documented path for unsigned extensions.
143
- - **`saveFrameToPng` is community-known**, not officially documented by Adobe. It works, but alpha edges can be imperfect on some comps.
144
- - **Long `run_jsx` loops freeze AE's UI**, because ExtendScript is single-threaded. Use `run_batch` for bulk work.
145
- - **Not covered:** render queue, footage import and replacement, AE preferences. Preferences are excluded by design — this tool animates, it doesn't reconfigure your app.
130
+ - The panel is unsigned, so loading it requires Adobe's `PlayerDebugMode`, which the setup step enables. This is Adobe's documented path for unsigned extensions.
131
+ - `saveFrameToPng` is community-known rather than officially documented. It works, but alpha edges can be imperfect on some comps.
132
+ - A long synchronous loop in `run_jsx` will freeze the After Effects UI. Use `run_batch` for bulk work.
133
+ - Not covered: the render queue, footage import and replacement, and application preferences.
146
134
 
147
135
  ## Development
148
136
 
@@ -150,26 +138,23 @@ Ask Claude to run `check_setup` first — it reports exactly which link in the c
150
138
  git clone https://github.com/Engine-Room-Games/after-effects-mcp.git
151
139
  cd after-effects-mcp
152
140
  npm install && npm run build
153
- npm run install:panel # or: ask Claude to run setup_panel
154
- npm run doctor # verify the whole chain
141
+ npm run install:panel
142
+ npm run doctor
155
143
  ```
156
144
 
157
- | Command | Does |
145
+ | Command | Purpose |
158
146
  |---|---|
159
- | `npm run build` | Compile TypeScript, concatenate the ExtendScript bundle |
160
- | `npm run build:jsx` | Rebuild only `bundle.jsx` (fast loop) |
161
- | `npm run watch:ts` | TypeScript watch mode |
162
- | `npm run doctor` | Diagnose the install |
163
- | `npm run inspect` | MCP Inspector against the server |
164
- | `npm run pack:check` | Preview the publishable tarball |
147
+ | `make build` | Compile TypeScript and the ExtendScript bundle |
148
+ | `make jsx` | Rebuild `bundle.jsx` and hot-reload it into a running After Effects |
149
+ | `make watch` | TypeScript watch mode |
150
+ | `make doctor` | Diagnose the install |
151
+ | `make verify` | Build, check version strings agree, dry-run the package |
152
+ | `make release` | Bump the patch version, tag, and publish |
153
+ | `make release 1.1.0` | Set an explicit version, tag, and publish |
165
154
 
166
- Reload ExtendScript changes without restarting AE:
167
-
168
- ```bash
169
- curl -X POST http://127.0.0.1:7777/reload-jsx
170
- ```
155
+ Adding a tool takes three edits: a zod schema in `packages/shared/src/schemas.ts`, a handler in the matching `packages/jsx/*.jsx` module, and a description in `packages/mcp-server/src/tools/descriptions.ts`. Registration is automatic.
171
156
 
172
- **Adding a tool** takes three edits: a zod schema in `packages/shared/src/schemas.ts`, a handler in the matching `packages/jsx/*.jsx` module, and a description in `packages/mcp-server/src/tools/descriptions.ts`. Registration is automatic. See [CLAUDE.md](CLAUDE.md) for the architecture in depth, the ExtendScript conventions, and the known-fragile areas.
157
+ See [CLAUDE.md](CLAUDE.md) for the architecture, the ExtendScript conventions, and the known-fragile areas.
173
158
 
174
159
  ## License
175
160
 
package/bin/server.js CHANGED
@@ -67,14 +67,14 @@ description: The visual and motion style for this project \u2014 palette, type,
67
67
  `;
68
68
  var CLAUDE_MD = `# {{NAME}}
69
69
 
70
- After Effects project folder. Claude drives After Effects directly from here.
70
+ After Effects project folder. The tools drive After Effects directly from here.
71
71
 
72
72
  ## How to work in this folder
73
73
 
74
74
  1. Open After Effects with the project you want to work on.
75
75
  2. Describe what you want in plain language \u2014 "build a lower third that says
76
76
  Chapter One and slides in from the left".
77
- 3. Claude reads the current state of the comp, builds it, and shows you.
77
+ 3. The current state of the comp is read, the change is made, and you see it.
78
78
 
79
79
  ## Style
80
80
 
@@ -99,10 +99,11 @@ var MCP_JSON = `{
99
99
  }
100
100
  }
101
101
  `;
102
+ var KNOWN_FLAGS = ["--no-mcp", "--with-mcp"];
102
103
  function parseInitArgs(argv) {
103
104
  const positional = argv.filter((a) => !a.startsWith("-"));
104
- const withMcp = argv.includes("--with-mcp");
105
- const unknown = argv.filter((a) => a.startsWith("-") && a !== "--with-mcp");
105
+ const withMcp = !argv.includes("--no-mcp");
106
+ const unknown = argv.filter((a) => a.startsWith("-") && !KNOWN_FLAGS.includes(a));
106
107
  if (unknown.length > 0) return { error: `Unknown option: ${unknown[0]}` };
107
108
  if (positional.length === 0) return { error: "Missing target directory." };
108
109
  if (positional.length > 1) return { error: `Expected one directory, got ${positional.length}.` };
@@ -113,7 +114,7 @@ function runInit(argv) {
113
114
  if ("error" in parsed) {
114
115
  process.stderr.write(`${parsed.error}
115
116
 
116
- Usage: npx @engine-room/after-effects-mcp init <directory> [--with-mcp]
117
+ Usage: npx @engine-room/after-effects-mcp init <directory> [--no-mcp]
117
118
  `);
118
119
  return 1;
119
120
  }
@@ -147,17 +148,16 @@ Delete them first, or choose a different directory.
147
148
  ` CLAUDE.md what this project is`,
148
149
  ` .claude/skills/house-style/SKILL.md your look \u2014 edit this first`,
149
150
  ` renders/ exports land here`,
150
- ...parsed.withMcp ? [` .mcp.json connects Claude to After Effects`] : [],
151
+ ...parsed.withMcp ? [` .mcp.json connects your client to After Effects`] : [],
151
152
  ``,
152
153
  `Next:`,
153
- ` 1. Open the folder in Claude Code: claude ${parsed.dir}`,
154
- ` 2. Fill in .claude/skills/house-style/SKILL.md with your palette, type and timing.`,
155
- ` 3. Open After Effects, then ask for what you want.`,
154
+ ` 1. Open the folder in your MCP client: cd ${parsed.dir}`,
155
+ ` 2. Open After Effects, then ask it to set up After Effects.`,
156
+ ` 3. Fill in .claude/skills/house-style/SKILL.md with your palette, type and timing.`,
156
157
  ...parsed.withMcp ? [] : [
157
158
  ``,
158
- `This folder has no .mcp.json \u2014 it assumes you installed the Claude Code`,
159
- `plugin, which already provides the After Effects tools. If you did not,`,
160
- `re-run with --with-mcp to add a project-level connection instead.`
159
+ `This folder has no .mcp.json, so your client must already provide the`,
160
+ `After Effects tools some other way.`
161
161
  ],
162
162
  ``
163
163
  ].join("\n");
@@ -1110,11 +1110,9 @@ var descriptions = {
1110
1110
  };
1111
1111
 
1112
1112
  // src/setup/check.ts
1113
- import { execFile } from "node:child_process";
1114
1113
  import crypto from "node:crypto";
1115
1114
  import fs4 from "node:fs";
1116
1115
  import path4 from "node:path";
1117
- import { promisify } from "node:util";
1118
1116
 
1119
1117
  // src/setup/paths.ts
1120
1118
  import fs3 from "node:fs";
@@ -1123,6 +1121,9 @@ import path3 from "node:path";
1123
1121
  import { createRequire } from "node:module";
1124
1122
  import { fileURLToPath } from "node:url";
1125
1123
  var BUNDLE_ID = "games.engine-room.ae-mcp";
1124
+ function isSupportedPlatform() {
1125
+ return process.platform === "darwin" || process.platform === "win32";
1126
+ }
1126
1127
  function packageRoot() {
1127
1128
  let dir = path3.dirname(fileURLToPath(import.meta.url));
1128
1129
  for (let i = 0; i < 8; i++) {
@@ -1147,6 +1148,10 @@ function panelSourceDir() {
1147
1148
  return null;
1148
1149
  }
1149
1150
  function cepExtensionsDir() {
1151
+ if (process.platform === "win32") {
1152
+ const appData = process.env.APPDATA ?? path3.join(os2.homedir(), "AppData", "Roaming");
1153
+ return path3.join(appData, "Adobe", "CEP", "extensions");
1154
+ }
1150
1155
  return path3.join(os2.homedir(), "Library", "Application Support", "Adobe", "CEP", "extensions");
1151
1156
  }
1152
1157
  function installedPanelDir() {
@@ -1178,27 +1183,84 @@ function copyRecursive(src, dst) {
1178
1183
  }
1179
1184
  }
1180
1185
 
1181
- // src/setup/check.ts
1186
+ // src/setup/platform.ts
1187
+ import { execFile } from "node:child_process";
1188
+ import { promisify } from "node:util";
1182
1189
  var exec = promisify(execFile);
1183
1190
  var CSXS_VERSIONS = [12, 11, 10, 9];
1184
- async function playerDebugMode() {
1191
+ async function isDebugModeOn() {
1185
1192
  for (const v of CSXS_VERSIONS) {
1186
1193
  try {
1187
- const { stdout } = await exec("defaults", ["read", `com.adobe.CSXS.${v}`, "PlayerDebugMode"]);
1188
- if (stdout.trim() === "1") return { on: true, detail: `enabled (CSXS.${v})` };
1194
+ if (process.platform === "win32") {
1195
+ const { stdout } = await exec("reg", [
1196
+ "query",
1197
+ `HKCU\\Software\\Adobe\\CSXS.${v}`,
1198
+ "/v",
1199
+ "PlayerDebugMode"
1200
+ ]);
1201
+ if (/PlayerDebugMode\s+REG_SZ\s+1\b/.test(stdout)) {
1202
+ return { on: true, detail: `enabled (CSXS.${v})` };
1203
+ }
1204
+ } else {
1205
+ const { stdout } = await exec("defaults", [
1206
+ "read",
1207
+ `com.adobe.CSXS.${v}`,
1208
+ "PlayerDebugMode"
1209
+ ]);
1210
+ if (stdout.trim() === "1") return { on: true, detail: `enabled (CSXS.${v})` };
1211
+ }
1189
1212
  } catch {
1190
1213
  }
1191
1214
  }
1192
1215
  return { on: false, detail: "not enabled for any CSXS version" };
1193
1216
  }
1194
- async function aeRunning() {
1217
+ async function enableDebugMode() {
1218
+ const enabled = [];
1219
+ for (const v of CSXS_VERSIONS) {
1220
+ try {
1221
+ if (process.platform === "win32") {
1222
+ await exec("reg", [
1223
+ "add",
1224
+ `HKCU\\Software\\Adobe\\CSXS.${v}`,
1225
+ "/v",
1226
+ "PlayerDebugMode",
1227
+ "/t",
1228
+ "REG_SZ",
1229
+ "/d",
1230
+ "1",
1231
+ "/f"
1232
+ ]);
1233
+ } else {
1234
+ await exec("defaults", [
1235
+ "write",
1236
+ `com.adobe.CSXS.${v}`,
1237
+ "PlayerDebugMode",
1238
+ "1"
1239
+ ]);
1240
+ }
1241
+ enabled.push(v);
1242
+ } catch {
1243
+ }
1244
+ }
1245
+ return enabled;
1246
+ }
1247
+ async function isAfterEffectsRunning() {
1195
1248
  try {
1249
+ if (process.platform === "win32") {
1250
+ const { stdout: stdout2 } = await exec("tasklist", ["/FI", "IMAGENAME eq AfterFX.exe", "/NH"]);
1251
+ return /AfterFX\.exe/i.test(stdout2);
1252
+ }
1196
1253
  const { stdout } = await exec("pgrep", ["-f", "Adobe After Effects"]);
1197
1254
  return stdout.trim().length > 0;
1198
1255
  } catch {
1199
1256
  return false;
1200
1257
  }
1201
1258
  }
1259
+ function debugModeLocation() {
1260
+ return process.platform === "win32" ? "the PlayerDebugMode value under HKEY_CURRENT_USER\\Software\\Adobe\\CSXS.*" : "Adobe's PlayerDebugMode preference";
1261
+ }
1262
+
1263
+ // src/setup/check.ts
1202
1264
  function sha256(file) {
1203
1265
  try {
1204
1266
  return crypto.createHash("sha256").update(fs4.readFileSync(file)).digest("hex");
@@ -1217,12 +1279,12 @@ async function bridgeReachable(port) {
1217
1279
  }
1218
1280
  async function checkSetup() {
1219
1281
  const checks = [];
1220
- const isMac = process.platform === "darwin";
1282
+ const supported = isSupportedPlatform();
1221
1283
  checks.push({
1222
1284
  name: "platform",
1223
- ok: isMac,
1285
+ ok: supported,
1224
1286
  detail: process.platform,
1225
- fix: isMac ? void 0 : "This server currently supports macOS only. Windows needs a different CEP install path and a registry edit."
1287
+ fix: supported ? void 0 : "After Effects runs only on macOS and Windows, so this server supports only those two."
1226
1288
  });
1227
1289
  const source = panelSourceDir();
1228
1290
  checks.push({
@@ -1231,12 +1293,12 @@ async function checkSetup() {
1231
1293
  detail: source ?? "not found in this installation",
1232
1294
  fix: source ? void 0 : "The package is missing its CEP panel assets \u2014 reinstall the server."
1233
1295
  });
1234
- const debugMode = await playerDebugMode();
1296
+ const debugMode = await isDebugModeOn();
1235
1297
  checks.push({
1236
1298
  name: "cepDebugMode",
1237
1299
  ok: debugMode.on,
1238
1300
  detail: debugMode.detail,
1239
- fix: debugMode.on ? void 0 : "Run the setup_panel tool. After Effects only loads unsigned panels when this Adobe preference is set."
1301
+ fix: debugMode.on ? void 0 : `Run the setup_panel tool. After Effects only loads unsigned panels when ${debugModeLocation()} is set.`
1240
1302
  });
1241
1303
  const installed = installedPanelDir();
1242
1304
  const isInstalled = fs4.existsSync(path4.join(installed, "CSXS", "manifest.xml"));
@@ -1257,7 +1319,7 @@ async function checkSetup() {
1257
1319
  fix: upToDate ? void 0 : "Run setup_panel to refresh it, then restart After Effects."
1258
1320
  });
1259
1321
  }
1260
- const running = await aeRunning();
1322
+ const running = await isAfterEffectsRunning();
1261
1323
  checks.push({
1262
1324
  name: "afterEffectsRunning",
1263
1325
  ok: running,
@@ -1272,6 +1334,14 @@ async function checkSetup() {
1272
1334
  detail: bridge.detail,
1273
1335
  fix: bridge.ok ? void 0 : "If the other checks pass, restart After Effects so the panel reloads."
1274
1336
  });
1337
+ if (bridge.ok && !isInstalled) {
1338
+ checks.push({
1339
+ name: "panelIdentity",
1340
+ ok: false,
1341
+ detail: `a panel is answering on port ${port}, but not the one at ${installed}`,
1342
+ fix: `An older install is serving the bridge. Look in ${cepExtensionsDir()} for a differently named folder, remove it, then run setup_panel and restart After Effects.`
1343
+ });
1344
+ }
1275
1345
  const ready = checks.every((c) => c.ok);
1276
1346
  return { ready, checks, nextSteps: buildNextSteps(checks, ready) };
1277
1347
  }
@@ -1287,7 +1357,13 @@ function buildNextSteps(checks, ready) {
1287
1357
  steps.push("Run the setup_panel tool \u2014 it installs the After Effects panel and enables the Adobe preference that lets AE load it.");
1288
1358
  }
1289
1359
  if (needsDebug) {
1290
- steps.push("Quit and reopen After Effects. If the panel still does not connect, restart the Mac once \u2014 the Adobe preference sometimes only takes effect after a reboot.");
1360
+ steps.push(
1361
+ process.platform === "win32" ? "Quit and reopen After Effects so it re-reads the registry." : "Quit and reopen After Effects. If the panel still does not connect, restart the Mac once \u2014 the Adobe preference sometimes only takes effect after a reboot."
1362
+ );
1363
+ }
1364
+ const identity = by("panelIdentity");
1365
+ if (identity && identity.ok === false) {
1366
+ steps.push(identity.fix);
1291
1367
  }
1292
1368
  if (by("afterEffectsRunning")?.ok === false) {
1293
1369
  steps.push("Open After Effects 2026.");
@@ -1302,38 +1378,15 @@ function buildNextSteps(checks, ready) {
1302
1378
  }
1303
1379
 
1304
1380
  // src/setup/install.ts
1305
- import { execFile as execFile2 } from "node:child_process";
1306
1381
  import fs5 from "node:fs";
1307
1382
  import path5 from "node:path";
1308
- import { promisify as promisify2 } from "node:util";
1309
- var exec2 = promisify2(execFile2);
1310
- var CSXS_VERSIONS2 = [12, 11, 10, 9];
1311
- async function isDebugModeOn() {
1312
- for (const v of CSXS_VERSIONS2) {
1313
- try {
1314
- const { stdout } = await exec2("defaults", ["read", `com.adobe.CSXS.${v}`, "PlayerDebugMode"]);
1315
- if (stdout.trim() === "1") return true;
1316
- } catch {
1317
- }
1318
- }
1319
- return false;
1320
- }
1321
- async function enableDebugMode() {
1322
- const enabled = [];
1323
- for (const v of CSXS_VERSIONS2) {
1324
- try {
1325
- await exec2("defaults", ["write", `com.adobe.CSXS.${v}`, "PlayerDebugMode", "1"]);
1326
- enabled.push(v);
1327
- } catch {
1328
- }
1329
- }
1330
- return enabled;
1331
- }
1332
1383
  async function installPanel(opts = {}) {
1333
1384
  const actions = [];
1334
1385
  const notes = [];
1335
- if (process.platform !== "darwin") {
1336
- throw new Error("setup_panel supports macOS only. On Windows the CEP extensions folder and the PlayerDebugMode registry key differ; install manually.");
1386
+ if (!isSupportedPlatform()) {
1387
+ throw new Error(
1388
+ `After Effects runs only on macOS and Windows, so setup_panel cannot install anything on ${process.platform}.`
1389
+ );
1337
1390
  }
1338
1391
  const source = panelSourceDir();
1339
1392
  if (!source) {
@@ -1377,16 +1430,16 @@ async function installPanel(opts = {}) {
1377
1430
  }
1378
1431
  let rebootRecommended = false;
1379
1432
  if (opts.enableDebugMode !== false) {
1380
- const wasOn = await isDebugModeOn();
1381
- if (wasOn) {
1382
- actions.push("Adobe's PlayerDebugMode preference was already enabled.");
1433
+ const existing2 = await isDebugModeOn();
1434
+ if (existing2.on) {
1435
+ actions.push(`PlayerDebugMode was already enabled \u2014 ${existing2.detail}.`);
1383
1436
  } else {
1384
1437
  const versions = await enableDebugMode();
1385
1438
  if (versions.length === 0) {
1386
- notes.push("Failed to set PlayerDebugMode. Without it After Effects will refuse to load this panel.");
1439
+ notes.push(`Failed to set ${debugModeLocation()}. Without it After Effects will refuse to load this panel.`);
1387
1440
  } else {
1388
1441
  actions.push(`Enabled PlayerDebugMode for CSXS ${versions.join(", ")} so AE will load the unsigned panel.`);
1389
- rebootRecommended = true;
1442
+ rebootRecommended = process.platform === "darwin";
1390
1443
  }
1391
1444
  }
1392
1445
  }
@@ -1423,7 +1476,7 @@ var GetJobSchema = schemas_exports.GetJob;
1423
1476
  var CancelJobSchema = schemas_exports.CancelJob;
1424
1477
  function createServer() {
1425
1478
  const server = new Server(
1426
- { name: "after-effects-mcp", version: "0.1.0" },
1479
+ { name: "after-effects-mcp", version: "0.1.1" },
1427
1480
  { capabilities: { tools: {}, logging: {} } }
1428
1481
  );
1429
1482
  const bridge = new HttpClient();
@@ -1578,11 +1631,12 @@ client (Claude Code, Claude Desktop, \u2026) starts it. You do not normally run
1578
1631
  by hand.
1579
1632
 
1580
1633
  Commands:
1581
- init <directory> [--with-mcp] Create a project folder with a house-style
1582
- skill you can edit. Pass --with-mcp if you
1583
- did not install the Claude Code plugin.
1584
- --help Show this message.
1585
- --version Print the version.
1634
+ init <directory> [--no-mcp] Create a project folder containing the client
1635
+ configuration and a house-style skill you can
1636
+ edit. Pass --no-mcp if your client already
1637
+ provides these tools.
1638
+ --help Show this message.
1639
+ --version Print the version.
1586
1640
  `;
1587
1641
  async function main() {
1588
1642
  const argv = process.argv.slice(2);
@@ -1609,7 +1663,7 @@ ${USAGE}`);
1609
1663
  await server.connect(transport);
1610
1664
  logger.info("MCP server running on stdio");
1611
1665
  }
1612
- var VERSION = "0.1.0";
1666
+ var VERSION = "0.1.1";
1613
1667
  main().catch((e) => {
1614
1668
  logger.error("fatal", e.message);
1615
1669
  process.exit(1);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@engine-room/after-effects-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
- "description": "MCP server that lets an AI agent drive Adobe After Effects comps, layers, keyframes, expressions, effects, text, shapes, masks and screenshots.",
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",
7
7
  "author": "Engine Room",
8
8
  "homepage": "https://github.com/Engine-Room-Games/after-effects-mcp#readme",
@@ -15,14 +15,16 @@
15
15
  "url": "https://github.com/Engine-Room-Games/after-effects-mcp/issues"
16
16
  },
17
17
  "keywords": [
18
- "mcp",
19
- "model-context-protocol",
20
18
  "after-effects",
21
19
  "adobe",
20
+ "ai",
22
21
  "animation",
23
22
  "motion-graphics",
24
- "extendscript",
25
- "claude"
23
+ "video",
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "claude",
27
+ "extendscript"
26
28
  ],
27
29
  "bin": {
28
30
  "after-effects-mcp": "bin/server.js"
@@ -37,7 +39,8 @@
37
39
  "LICENSE"
38
40
  ],
39
41
  "os": [
40
- "darwin"
42
+ "darwin",
43
+ "win32"
41
44
  ],
42
45
  "engines": {
43
46
  "node": ">=20"
@@ -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.0"
2
+ <ExtensionManifest Version="11.0" ExtensionBundleId="games.engine-room.ae-mcp" ExtensionBundleVersion="0.1.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.0" />
5
+ <Extension Id="games.engine-room.ae-mcp.panel" Version="0.1.1" />
6
6
  </ExtensionList>
7
7
  <ExecutionEnvironment>
8
8
  <HostList>
@@ -10,7 +10,6 @@
10
10
  var fs = require("fs");
11
11
  var os = require("os");
12
12
  var http = require("http");
13
- var execFileSync = require("child_process").execFileSync;
14
13
  var WebSocket;
15
14
  try { WebSocket = require("ws"); }
16
15
  catch (e) {
@@ -145,45 +144,30 @@
145
144
  })();
146
145
  });
147
146
  }
148
- // Shrink the PNG in place with `sips` (macOS built-in, no npm dependency).
149
- // saveFrameToPng always writes at full comp resolution, so this is the only
150
- // place `downsample` can be honoured. Returns the dimensions actually
151
- // produced never claims a resize that did not happen.
152
- function shrinkPng(file, width, height, factor) {
153
- var outW = Math.max(1, Math.round(width / factor));
154
- var outH = Math.max(1, Math.round(height / factor));
155
- try {
156
- // sips takes height then width.
157
- execFileSync("sips", ["-z", String(outH), String(outW), file], { stdio: "ignore" });
158
- return { width: outW, height: outH, downsample: factor };
159
- } catch (e) {
160
- log("warn", "downsample " + factor + "x failed, returning full resolution: " + e.message);
161
- return {
162
- width: width,
163
- height: height,
164
- downsample: 1,
165
- warning: "downsample=" + factor + " was requested but `sips` failed (" + e.message +
166
- "); returning the full-resolution frame instead.",
167
- };
168
- }
147
+ // Read the true pixel dimensions out of the PNG's IHDR chunk rather than
148
+ // computing them, so what we report is always what the client received.
149
+ function pngDimensions(buf) {
150
+ if (buf.length < 24) return null;
151
+ if (buf.readUInt32BE(12) !== 0x49484452) return null; // "IHDR"
152
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
169
153
  }
170
154
 
171
- function readPngAsBase64(file, downsample, width, height) {
172
- return waitForPngFile(file, 5000).then(function () {
173
- // Resize only after the file has finished being written.
174
- var dims = (downsample && downsample > 1)
175
- ? shrinkPng(file, width, height, downsample)
176
- : { width: width, height: height, downsample: 1 };
155
+ // Downsampling happens in ExtendScript via the comp's resolutionFactor, so
156
+ // the file on disk is already the right size by the time we get here.
157
+ function readPngAsBase64(file) {
158
+ // A cold render of a heavy 4K comp can take well over 15s — measured on a
159
+ // real project. Five seconds silently failed screenshots that were simply
160
+ // still rendering.
161
+ return waitForPngFile(file, 120000).then(function () {
177
162
  var buf = fs.readFileSync(file);
178
163
  var b64 = buf.toString("base64");
179
164
  try { fs.unlinkSync(file); } catch (e) {}
165
+ var dims = pngDimensions(buf);
180
166
  return {
181
167
  base64: b64,
182
168
  bytes: buf.length,
183
- width: dims.width,
184
- height: dims.height,
185
- downsample: dims.downsample,
186
- warning: dims.warning,
169
+ width: dims ? dims.width : null,
170
+ height: dims ? dims.height : null,
187
171
  };
188
172
  });
189
173
  }
@@ -214,15 +198,16 @@
214
198
  function handleOp(op, args, progressToken) {
215
199
  // Vision ops: run JSX, then read PNG and base64-encode on Node side.
216
200
  if (op === "screenshot_frame" || op === "screenshot_layer") {
217
- var factor = (args && args.downsample) ? Math.round(args.downsample) : 1;
218
201
  return runOp(op, args).then(function (info) {
219
- return readPngAsBase64(info.path, factor, info.width, info.height).then(function (img) {
220
- var out = {
202
+ return readPngAsBase64(info.path).then(function (img) {
203
+ return {
204
+ // Dimensions come from the PNG itself, not from arithmetic on the
205
+ // comp size, so they cannot disagree with the image sent.
221
206
  width: img.width,
222
207
  height: img.height,
223
208
  fullWidth: info.width,
224
209
  fullHeight: info.height,
225
- downsample: img.downsample,
210
+ downsample: info.downsample,
226
211
  time: info.time,
227
212
  compId: info.compId,
228
213
  layerId: info.layerId,
@@ -230,8 +215,6 @@
230
215
  base64: img.base64,
231
216
  bytes: img.bytes,
232
217
  };
233
- if (img.warning) out.warning = img.warning;
234
- return out;
235
218
  });
236
219
  });
237
220
  }
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bundle. Do not edit directly — edit files in packages/jsx/.
2
- // Generated 2026-08-10T14:20:22.883Z
2
+ // Generated 2026-08-10T15:26:38.133Z
3
3
 
4
4
  // ===== core.jsx =====
5
5
 
@@ -1146,14 +1146,47 @@ function __tmpPngPath() {
1146
1146
  return folder.fsName + "/" + name;
1147
1147
  }
1148
1148
 
1149
+ function __clampDownsample(v) {
1150
+ if (v === undefined || v === null) return 1;
1151
+ var n = Math.round(v);
1152
+ if (!(n > 1)) return 1;
1153
+ return n > 8 ? 8 : n;
1154
+ }
1155
+
1156
+ // saveFrameToPng honours the comp's resolutionFactor, so AE can render the
1157
+ // reduced frame directly instead of writing full size and resampling
1158
+ // afterwards. That is faster (a quarter of the pixels at factor 2) and needs no
1159
+ // external image tool, which is what makes it work off macOS.
1160
+ function __saveFrameAt(comp, time, file, factor) {
1161
+ if (factor <= 1) {
1162
+ comp.saveFrameToPng(time, file);
1163
+ return;
1164
+ }
1165
+ var previous = comp.resolutionFactor;
1166
+ try {
1167
+ comp.resolutionFactor = [factor, factor];
1168
+ comp.saveFrameToPng(time, file);
1169
+ } finally {
1170
+ // Restore unconditionally — a failed render must never leave the user
1171
+ // looking at a half-resolution comp.
1172
+ comp.resolutionFactor = previous;
1173
+ }
1174
+ }
1175
+
1149
1176
  OPS.screenshot_frame = noUndo(function (args) {
1150
1177
  var c = getCompById(args.compId);
1151
1178
  var t = (args.time !== undefined && args.time !== null) ? args.time : c.time;
1179
+ var ds = __clampDownsample(args.downsample);
1152
1180
  var path = __tmpPngPath();
1153
1181
  var f = new File(path);
1154
1182
  // saveFrameToPng is async-ish; the panel polls the file's existence/size.
1155
- c.saveFrameToPng(t, f);
1156
- return { path: path, width: c.width, height: c.height, time: t, compId: c.id };
1183
+ __saveFrameAt(c, t, f, ds);
1184
+ return {
1185
+ path: path,
1186
+ width: c.width, height: c.height,
1187
+ downsample: ds,
1188
+ time: t, compId: c.id
1189
+ };
1157
1190
  });
1158
1191
 
1159
1192
  OPS.screenshot_layer = noUndo(function (args) {
@@ -1168,10 +1201,11 @@ OPS.screenshot_layer = noUndo(function (args) {
1168
1201
  ll.solo = false;
1169
1202
  }
1170
1203
  l.solo = true;
1204
+ var ds = __clampDownsample(args.downsample);
1171
1205
  var path = __tmpPngPath();
1172
1206
  var f = new File(path);
1173
1207
  try {
1174
- c.saveFrameToPng(t, f);
1208
+ __saveFrameAt(c, t, f, ds);
1175
1209
  } finally {
1176
1210
  // restore
1177
1211
  l.solo = false;
@@ -1179,7 +1213,12 @@ OPS.screenshot_layer = noUndo(function (args) {
1179
1213
  try { c.layer(prevSolo[j].idx).solo = prevSolo[j].solo; } catch (e) {}
1180
1214
  }
1181
1215
  }
1182
- return { path: path, width: c.width, height: c.height, time: t, compId: c.id, layerId: l.id };
1216
+ return {
1217
+ path: path,
1218
+ width: c.width, height: c.height,
1219
+ downsample: ds,
1220
+ time: t, compId: c.id, layerId: l.id
1221
+ };
1183
1222
  });
1184
1223
 
1185
1224
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineroom/ae-panel",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": true,
5
5
  "description": "Invisible CEP extension hosting an HTTP+WS bridge inside After Effects.",
6
6
  "dependencies": {