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

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
- ```
11
-
12
- **Requirements:** macOS · After Effects 2026 · [Node.js 20+](https://nodejs.org)
7
+ Requires After Effects 2026 and [Node.js 20+](https://nodejs.org).
13
8
 
14
- ---
9
+ ## Getting started
15
10
 
16
- ## Install
11
+ ### 1. Create a project folder
17
12
 
18
- ### Claude Code (recommended)
13
+ In a terminal:
19
14
 
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.
20
+ Open that folder in your AI client. It contains everything the client needs to find the tools.
26
21
 
27
- ### Any other MCP client
28
-
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,130 @@ 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
68
+
69
+ Two things to update: the tools, and the panel inside After Effects. The panel does not update on its own.
70
+
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.
76
+
77
+ ## Tools
83
78
 
84
79
  | Group | Tools |
85
80
  |---|---|
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
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
+ | Issues (3) | `list_known_issues`, `log_issue`, `mark_issue_reported` |
98
+
99
+ A few notes on the ones that behave differently from the rest:
100
+
101
+ - `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.
102
+ - `run_batch` runs many operations in a single pass and counts as one undo step. Long batches stream progress.
103
+ - `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.
104
+ - `run_jsx` runs arbitrary ExtendScript for anything the other tools do not cover.
105
+ - `log_issue` and `list_known_issues` are the notebook described below.
106
+
107
+ ## When something goes wrong
108
+
109
+ These tools have rough edges. When the AI hits one and works out a way around
110
+ it, it writes the problem and the fix into a notebook in your project folder, at
111
+ `.ae-mcp/issues/` — plain text files you can read or delete. The next session
112
+ reads that notebook before guessing, so the same twenty minutes are never spent
113
+ twice on the same project.
114
+
115
+ The folder ignores itself, so it stays out of version control without you doing
116
+ anything.
117
+
118
+ If the problem looks like ours rather than yours, the AI will say so at the end
119
+ of its reply and offer to pass it on. You can also start that yourself:
112
120
 
113
121
  ```
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
122
+ /report-ae-issue
124
123
  ```
125
124
 
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.
125
+ It writes the report, shows it to you, and only sends it once you say yes.
126
+ Nothing about your own work — comp names, file paths, clients — goes into it.
127
+ Sending needs the [GitHub CLI](https://cli.github.com); without it you get a
128
+ prefilled link to click instead.
127
129
 
128
130
  ## Troubleshooting
129
131
 
130
- Ask Claude to run `check_setup` first it reports exactly which link in the chain is broken and what to do about it.
132
+ Ask the AI to check the After Effects setup. It reports which part is broken and what to do about it.
131
133
 
132
- | Symptom | Cause |
134
+ | Symptom | Cause and fix |
133
135
  |---|---|
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**. |
136
+ | "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. |
137
+ | 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. |
138
+ | Panel never loads, setup looks correct | On macOS, reboot once. Some builds cache the Adobe setting until a restart. |
139
+ | 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. |
140
+ | Need the panel's own log | In AE: Window → Extensions → AE MCP Bridge. |
141
+
142
+ ## Platforms
143
+
144
+ macOS and Windows. Two things differ, and are handled for you:
145
+
146
+ | | macOS | Windows |
147
+ |---|---|---|
148
+ | Panel location | `~/Library/Application Support/Adobe/CEP/extensions` | `%APPDATA%\Adobe\CEP\extensions` |
149
+ | Unsigned-panel setting | `defaults` preference | `HKCU\Software\Adobe\CSXS.*` registry value |
150
+
151
+ macOS is the more exercised of the two; [issue reports](https://github.com/Engine-Room-Games/after-effects-mcp/issues) are welcome.
138
152
 
139
153
  ## Limitations
140
154
 
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.
155
+ - 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.
156
+ - `saveFrameToPng` is community-known rather than officially documented. It works, but alpha edges can be imperfect on some comps.
157
+ - A long synchronous loop in `run_jsx` will freeze the After Effects UI. Use `run_batch` for bulk work.
158
+ - Not covered: the render queue, footage import and replacement, and application preferences.
146
159
 
147
160
  ## Development
148
161
 
@@ -150,26 +163,23 @@ Ask Claude to run `check_setup` first — it reports exactly which link in the c
150
163
  git clone https://github.com/Engine-Room-Games/after-effects-mcp.git
151
164
  cd after-effects-mcp
152
165
  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
166
+ npm run install:panel
167
+ npm run doctor
155
168
  ```
156
169
 
157
- | Command | Does |
170
+ | Command | Purpose |
158
171
  |---|---|
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 |
172
+ | `make build` | Compile TypeScript and the ExtendScript bundle |
173
+ | `make jsx` | Rebuild `bundle.jsx` and hot-reload it into a running After Effects |
174
+ | `make watch` | TypeScript watch mode |
175
+ | `make doctor` | Diagnose the install |
176
+ | `make verify` | Build, check version strings agree, dry-run the package |
177
+ | `make release` | Bump the patch version, tag, and publish |
178
+ | `make release 1.1.0` | Set an explicit version, tag, and publish |
165
179
 
166
- Reload ExtendScript changes without restarting AE:
167
-
168
- ```bash
169
- curl -X POST http://127.0.0.1:7777/reload-jsx
170
- ```
180
+ 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
181
 
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.
182
+ See [CLAUDE.md](CLAUDE.md) for the architecture, the ExtendScript conventions, and the known-fragile areas.
173
183
 
174
184
  ## License
175
185
 
package/bin/server.js CHANGED
@@ -65,16 +65,49 @@ description: The visual and motion style for this project \u2014 palette, type,
65
65
  - _Anything that should always or never happen. E.g. "never put text directly on
66
66
  footage \u2014 always on a rounded chip", "keep total runtime under 8 seconds"._
67
67
  `;
68
+ var REPORT_COMMAND = `---
69
+ description: Send a problem you hit with the After Effects tools to the people who maintain them
70
+ argument-hint: "[what went wrong, in your own words]"
71
+ ---
72
+
73
+ # Report a problem with the After Effects tools
74
+
75
+ Assume the person you are helping is a motion designer, not a developer, and may
76
+ never have used GitHub. Do the technical part for them.
77
+
78
+ 1. **Find what to report.** Call \`list_known_issues\` with \`status: "unreported"\`.
79
+ It returns what earlier sessions wrote down, plus \`repo\`, \`newIssueUrl\`,
80
+ \`serverVersion\` and \`platform\`. List the entries in plain sentences \u2014 not raw
81
+ titles \u2014 and ask which to send. If there is nothing recorded but \`$ARGUMENTS\`
82
+ describes a problem, ask what they were doing and what happened, then
83
+ \`log_issue\` it first. If there is nothing at all, say so and stop.
84
+
85
+ 2. **Draft it short.** Title: one concrete line. Body: **What happens** (the
86
+ failing call and exact error), **Why** if known, **Workaround**, and
87
+ **Environment** (\`after-effects-mcp <serverVersion> \xB7 <platform> \xB7 After
88
+ Effects 2026\`). Leave out their own work \u2014 comp names, file paths, client
89
+ names, anything about the video.
90
+
91
+ 3. **Show it and ask.** This posts publicly, so get a real yes.
92
+
93
+ 4. **Send it.** \`gh issue create --repo <repo> --title "..." --body "..."\`. If
94
+ \`gh\` is missing or not logged in, do not install it \u2014 build a prefilled link
95
+ instead by URL-encoding the title and body onto \`<newIssueUrl>\` as
96
+ \`?title=\u2026&body=\u2026\`, and tell them to open it and press the green button.
97
+
98
+ 5. **Close the loop.** On success call \`mark_issue_reported\` with the entry id
99
+ and URL, then give them the link. If they decline, leave the entry unreported.
100
+ `;
68
101
  var CLAUDE_MD = `# {{NAME}}
69
102
 
70
- After Effects project folder. Claude drives After Effects directly from here.
103
+ After Effects project folder. The tools drive After Effects directly from here.
71
104
 
72
105
  ## How to work in this folder
73
106
 
74
107
  1. Open After Effects with the project you want to work on.
75
108
  2. Describe what you want in plain language \u2014 "build a lower third that says
76
109
  Chapter One and slides in from the left".
77
- 3. Claude reads the current state of the comp, builds it, and shows you.
110
+ 3. The current state of the comp is read, the change is made, and you see it.
78
111
 
79
112
  ## Style
80
113
 
@@ -82,6 +115,14 @@ The look of everything built here is defined in
82
115
  \`.claude/skills/house-style/SKILL.md\`. Edit that file to change the defaults \u2014
83
116
  palette, type, timing, layout. It is read automatically.
84
117
 
118
+ ## When a tool misbehaves
119
+
120
+ Check \`list_known_issues\` before guessing \u2014 an earlier session may already have
121
+ solved it. When you work out a fix for something that cost real time and was the
122
+ tool's fault rather than yours, record it with \`log_issue\` so the next session
123
+ does not pay for it again, and offer at the end of your reply to pass it on.
124
+ \`/report-ae-issue\` sends it to the maintainers.
125
+
85
126
  ## Conventions for this project
86
127
 
87
128
  <!-- Anything specific to this project rather than to your general style:
@@ -99,10 +140,11 @@ var MCP_JSON = `{
99
140
  }
100
141
  }
101
142
  `;
143
+ var KNOWN_FLAGS = ["--no-mcp", "--with-mcp"];
102
144
  function parseInitArgs(argv) {
103
145
  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");
146
+ const withMcp = !argv.includes("--no-mcp");
147
+ const unknown = argv.filter((a) => a.startsWith("-") && !KNOWN_FLAGS.includes(a));
106
148
  if (unknown.length > 0) return { error: `Unknown option: ${unknown[0]}` };
107
149
  if (positional.length === 0) return { error: "Missing target directory." };
108
150
  if (positional.length > 1) return { error: `Expected one directory, got ${positional.length}.` };
@@ -113,7 +155,7 @@ function runInit(argv) {
113
155
  if ("error" in parsed) {
114
156
  process.stderr.write(`${parsed.error}
115
157
 
116
- Usage: npx @engine-room/after-effects-mcp init <directory> [--with-mcp]
158
+ Usage: npx @engine-room/after-effects-mcp init <directory> [--no-mcp]
117
159
  `);
118
160
  return 1;
119
161
  }
@@ -122,6 +164,7 @@ Usage: npx @engine-room/after-effects-mcp init <directory> [--with-mcp]
122
164
  const files = [
123
165
  ["CLAUDE.md", CLAUDE_MD.replace("{{NAME}}", name)],
124
166
  [path.join(".claude", "skills", "house-style", "SKILL.md"), HOUSE_STYLE],
167
+ [path.join(".claude", "commands", "report-ae-issue.md"), REPORT_COMMAND],
125
168
  [path.join("renders", ".gitkeep"), ""]
126
169
  ];
127
170
  if (parsed.withMcp) files.push([".mcp.json", MCP_JSON]);
@@ -146,18 +189,18 @@ Delete them first, or choose a different directory.
146
189
  ``,
147
190
  ` CLAUDE.md what this project is`,
148
191
  ` .claude/skills/house-style/SKILL.md your look \u2014 edit this first`,
192
+ ` .claude/commands/report-ae-issue.md /report-ae-issue \u2014 tell the maintainers something broke`,
149
193
  ` renders/ exports land here`,
150
- ...parsed.withMcp ? [` .mcp.json connects Claude to After Effects`] : [],
194
+ ...parsed.withMcp ? [` .mcp.json connects your client to After Effects`] : [],
151
195
  ``,
152
196
  `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.`,
197
+ ` 1. Open the folder in your MCP client: cd ${parsed.dir}`,
198
+ ` 2. Open After Effects, then ask it to set up After Effects.`,
199
+ ` 3. Fill in .claude/skills/house-style/SKILL.md with your palette, type and timing.`,
156
200
  ...parsed.withMcp ? [] : [
157
201
  ``,
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.`
202
+ `This folder has no .mcp.json, so your client must already provide the`,
203
+ `After Effects tools some other way.`
161
204
  ],
162
205
  ``
163
206
  ].join("\n");
@@ -262,7 +305,10 @@ __export(schemas_exports, {
262
305
  ListAvailableEffects: () => ListAvailableEffects,
263
306
  ListComps: () => ListComps,
264
307
  ListEffects: () => ListEffects,
308
+ ListKnownIssues: () => ListKnownIssues,
265
309
  ListLayers: () => ListLayers,
310
+ LogIssue: () => LogIssue,
311
+ MarkIssueReported: () => MarkIssueReported,
266
312
  OpSchemas: () => OpSchemas,
267
313
  ParentLayer: () => ParentLayer,
268
314
  PropertyPath: () => PropertyPath,
@@ -669,6 +715,21 @@ var SetupPanel = z2.object({
669
715
  enableDebugMode: z2.boolean().default(true).optional().describe("Also enable Adobe's PlayerDebugMode preference, which AE requires to load this unsigned panel. Default true."),
670
716
  force: z2.boolean().default(false).optional().describe("Replace an existing symlinked (development) install with a copy. Default false.")
671
717
  }).strict();
718
+ var LogIssue = z2.object({
719
+ title: z2.string().min(3).describe("One line naming the problem, specific enough to recognise again. Becomes the entry's id."),
720
+ symptom: z2.string().min(3).describe("What went wrong, including the exact error text and the call that produced it."),
721
+ workaround: z2.string().min(3).describe("What actually worked \u2014 concrete enough for the next session to apply without rediscovering it."),
722
+ cause: z2.string().optional().describe("Why it happens, if you worked it out."),
723
+ tools: z2.array(z2.string()).optional().describe("Tool names involved, e.g. ['set_temporal_ease'].")
724
+ }).strict();
725
+ var ListKnownIssues = z2.object({
726
+ status: z2.enum(["all", "unreported", "reported"]).default("all").optional(),
727
+ tool: z2.string().optional().describe("Only entries about this tool, e.g. 'set_temporal_ease'. Omit for everything.")
728
+ }).strict();
729
+ var MarkIssueReported = z2.object({
730
+ id: z2.string().describe("The entry id returned by log_issue or list_known_issues."),
731
+ url: z2.string().optional().describe("Link to the issue that was opened.")
732
+ }).strict();
672
733
  var AwaitJob = z2.object({ jobId: z2.string(), timeoutMs: z2.number().int().positive().default(6e5).optional() });
673
734
  var GetJob = z2.object({ jobId: z2.string() });
674
735
  var CancelJob = z2.object({ jobId: z2.string() });
@@ -748,7 +809,11 @@ var OpSchemas = {
748
809
  cancel_job: CancelJob,
749
810
  // setup
750
811
  check_setup: CheckSetup,
751
- setup_panel: SetupPanel
812
+ setup_panel: SetupPanel,
813
+ // issue journal
814
+ log_issue: LogIssue,
815
+ list_known_issues: ListKnownIssues,
816
+ mark_issue_reported: MarkIssueReported
752
817
  };
753
818
 
754
819
  // src/util/errors.ts
@@ -1106,15 +1171,17 @@ var descriptions = {
1106
1171
  cancel_job: "Set cancel flag; chunked loop stops at next boundary.",
1107
1172
  // ---------- setup ----------
1108
1173
  check_setup: "Diagnose the After Effects connection: panel installed, up to date, Adobe debug preference on, AE running, bridge answering. Read-only and safe to call any time. Call this FIRST whenever another tool reports it cannot reach After Effects, then relay `nextSteps` to the user in plain language.",
1109
- setup_panel: "Install or refresh the After Effects panel and enable the Adobe preference AE needs to load it. Run this when check_setup reports the panel is missing or out of date. It writes to the user's Adobe CEP extensions folder and sets a user-level Adobe preference \u2014 tell the user what it will do before calling it. Afterwards, AE must be restarted; if the preference was newly enabled, a one-time Mac reboot may also be needed."
1174
+ setup_panel: "Install or refresh the After Effects panel and enable the Adobe preference AE needs to load it. Run this when check_setup reports the panel is missing or out of date. It writes to the user's Adobe CEP extensions folder and sets a user-level Adobe preference \u2014 tell the user what it will do before calling it. Afterwards, AE must be restarted; if the preference was newly enabled, a one-time Mac reboot may also be needed.",
1175
+ // ---------- issue journal ----------
1176
+ list_known_issues: "Problems earlier sessions hit with these tools, with the workarounds that worked. Read it when a tool fails in a way you don't immediately understand \u2014 pass `tool` to narrow it to that one \u2014 and before nontrivial work. It can save you rediscovering a fix that already cost someone an hour. Also returns the repo and server version needed to report one.",
1177
+ log_issue: "Record a problem you hit and the workaround that got past it, so the next session doesn't rediscover it. Log only what cost real effort and will recur: a tool failing for a non-obvious reason, an argument shape the schema didn't imply, AE behaving unlike the docs. Not your own typos, not one-off user mistakes. Call list_known_issues first and reuse the same title to extend an existing entry rather than duplicating it. If the result comes back with reported:false, then AFTER you have finished the actual work, close your reply by telling the user in plain language that something took much longer than it should have and offering to pass it to the people who maintain this tool \u2014 phrase it for a motion designer, in terms of what actually happened, and don't say 'GitHub issue' or 'bug report' unless they say it first.",
1178
+ mark_issue_reported: "Record that a journal entry has been sent to the maintainers, with the resulting URL. Call it only once the issue really exists, so later sessions don't ask the user to report the same thing twice."
1110
1179
  };
1111
1180
 
1112
1181
  // src/setup/check.ts
1113
- import { execFile } from "node:child_process";
1114
1182
  import crypto from "node:crypto";
1115
1183
  import fs4 from "node:fs";
1116
1184
  import path4 from "node:path";
1117
- import { promisify } from "node:util";
1118
1185
 
1119
1186
  // src/setup/paths.ts
1120
1187
  import fs3 from "node:fs";
@@ -1123,6 +1190,9 @@ import path3 from "node:path";
1123
1190
  import { createRequire } from "node:module";
1124
1191
  import { fileURLToPath } from "node:url";
1125
1192
  var BUNDLE_ID = "games.engine-room.ae-mcp";
1193
+ function isSupportedPlatform() {
1194
+ return process.platform === "darwin" || process.platform === "win32";
1195
+ }
1126
1196
  function packageRoot() {
1127
1197
  let dir = path3.dirname(fileURLToPath(import.meta.url));
1128
1198
  for (let i = 0; i < 8; i++) {
@@ -1133,6 +1203,14 @@ function packageRoot() {
1133
1203
  }
1134
1204
  return path3.dirname(fileURLToPath(import.meta.url));
1135
1205
  }
1206
+ function packageVersion() {
1207
+ try {
1208
+ const pkg = JSON.parse(fs3.readFileSync(path3.join(packageRoot(), "package.json"), "utf8"));
1209
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
1210
+ } catch {
1211
+ return "unknown";
1212
+ }
1213
+ }
1136
1214
  function panelSourceDir() {
1137
1215
  const candidates = [
1138
1216
  // The live workspace copy comes first so a git checkout always installs
@@ -1147,6 +1225,10 @@ function panelSourceDir() {
1147
1225
  return null;
1148
1226
  }
1149
1227
  function cepExtensionsDir() {
1228
+ if (process.platform === "win32") {
1229
+ const appData = process.env.APPDATA ?? path3.join(os2.homedir(), "AppData", "Roaming");
1230
+ return path3.join(appData, "Adobe", "CEP", "extensions");
1231
+ }
1150
1232
  return path3.join(os2.homedir(), "Library", "Application Support", "Adobe", "CEP", "extensions");
1151
1233
  }
1152
1234
  function installedPanelDir() {
@@ -1178,27 +1260,84 @@ function copyRecursive(src, dst) {
1178
1260
  }
1179
1261
  }
1180
1262
 
1181
- // src/setup/check.ts
1263
+ // src/setup/platform.ts
1264
+ import { execFile } from "node:child_process";
1265
+ import { promisify } from "node:util";
1182
1266
  var exec = promisify(execFile);
1183
1267
  var CSXS_VERSIONS = [12, 11, 10, 9];
1184
- async function playerDebugMode() {
1268
+ async function isDebugModeOn() {
1185
1269
  for (const v of CSXS_VERSIONS) {
1186
1270
  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})` };
1271
+ if (process.platform === "win32") {
1272
+ const { stdout } = await exec("reg", [
1273
+ "query",
1274
+ `HKCU\\Software\\Adobe\\CSXS.${v}`,
1275
+ "/v",
1276
+ "PlayerDebugMode"
1277
+ ]);
1278
+ if (/PlayerDebugMode\s+REG_SZ\s+1\b/.test(stdout)) {
1279
+ return { on: true, detail: `enabled (CSXS.${v})` };
1280
+ }
1281
+ } else {
1282
+ const { stdout } = await exec("defaults", [
1283
+ "read",
1284
+ `com.adobe.CSXS.${v}`,
1285
+ "PlayerDebugMode"
1286
+ ]);
1287
+ if (stdout.trim() === "1") return { on: true, detail: `enabled (CSXS.${v})` };
1288
+ }
1189
1289
  } catch {
1190
1290
  }
1191
1291
  }
1192
1292
  return { on: false, detail: "not enabled for any CSXS version" };
1193
1293
  }
1194
- async function aeRunning() {
1294
+ async function enableDebugMode() {
1295
+ const enabled = [];
1296
+ for (const v of CSXS_VERSIONS) {
1297
+ try {
1298
+ if (process.platform === "win32") {
1299
+ await exec("reg", [
1300
+ "add",
1301
+ `HKCU\\Software\\Adobe\\CSXS.${v}`,
1302
+ "/v",
1303
+ "PlayerDebugMode",
1304
+ "/t",
1305
+ "REG_SZ",
1306
+ "/d",
1307
+ "1",
1308
+ "/f"
1309
+ ]);
1310
+ } else {
1311
+ await exec("defaults", [
1312
+ "write",
1313
+ `com.adobe.CSXS.${v}`,
1314
+ "PlayerDebugMode",
1315
+ "1"
1316
+ ]);
1317
+ }
1318
+ enabled.push(v);
1319
+ } catch {
1320
+ }
1321
+ }
1322
+ return enabled;
1323
+ }
1324
+ async function isAfterEffectsRunning() {
1195
1325
  try {
1326
+ if (process.platform === "win32") {
1327
+ const { stdout: stdout2 } = await exec("tasklist", ["/FI", "IMAGENAME eq AfterFX.exe", "/NH"]);
1328
+ return /AfterFX\.exe/i.test(stdout2);
1329
+ }
1196
1330
  const { stdout } = await exec("pgrep", ["-f", "Adobe After Effects"]);
1197
1331
  return stdout.trim().length > 0;
1198
1332
  } catch {
1199
1333
  return false;
1200
1334
  }
1201
1335
  }
1336
+ function debugModeLocation() {
1337
+ return process.platform === "win32" ? "the PlayerDebugMode value under HKEY_CURRENT_USER\\Software\\Adobe\\CSXS.*" : "Adobe's PlayerDebugMode preference";
1338
+ }
1339
+
1340
+ // src/setup/check.ts
1202
1341
  function sha256(file) {
1203
1342
  try {
1204
1343
  return crypto.createHash("sha256").update(fs4.readFileSync(file)).digest("hex");
@@ -1217,12 +1356,12 @@ async function bridgeReachable(port) {
1217
1356
  }
1218
1357
  async function checkSetup() {
1219
1358
  const checks = [];
1220
- const isMac = process.platform === "darwin";
1359
+ const supported = isSupportedPlatform();
1221
1360
  checks.push({
1222
1361
  name: "platform",
1223
- ok: isMac,
1362
+ ok: supported,
1224
1363
  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."
1364
+ fix: supported ? void 0 : "After Effects runs only on macOS and Windows, so this server supports only those two."
1226
1365
  });
1227
1366
  const source = panelSourceDir();
1228
1367
  checks.push({
@@ -1231,12 +1370,12 @@ async function checkSetup() {
1231
1370
  detail: source ?? "not found in this installation",
1232
1371
  fix: source ? void 0 : "The package is missing its CEP panel assets \u2014 reinstall the server."
1233
1372
  });
1234
- const debugMode = await playerDebugMode();
1373
+ const debugMode = await isDebugModeOn();
1235
1374
  checks.push({
1236
1375
  name: "cepDebugMode",
1237
1376
  ok: debugMode.on,
1238
1377
  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."
1378
+ fix: debugMode.on ? void 0 : `Run the setup_panel tool. After Effects only loads unsigned panels when ${debugModeLocation()} is set.`
1240
1379
  });
1241
1380
  const installed = installedPanelDir();
1242
1381
  const isInstalled = fs4.existsSync(path4.join(installed, "CSXS", "manifest.xml"));
@@ -1257,7 +1396,7 @@ async function checkSetup() {
1257
1396
  fix: upToDate ? void 0 : "Run setup_panel to refresh it, then restart After Effects."
1258
1397
  });
1259
1398
  }
1260
- const running = await aeRunning();
1399
+ const running = await isAfterEffectsRunning();
1261
1400
  checks.push({
1262
1401
  name: "afterEffectsRunning",
1263
1402
  ok: running,
@@ -1272,6 +1411,14 @@ async function checkSetup() {
1272
1411
  detail: bridge.detail,
1273
1412
  fix: bridge.ok ? void 0 : "If the other checks pass, restart After Effects so the panel reloads."
1274
1413
  });
1414
+ if (bridge.ok && !isInstalled) {
1415
+ checks.push({
1416
+ name: "panelIdentity",
1417
+ ok: false,
1418
+ detail: `a panel is answering on port ${port}, but not the one at ${installed}`,
1419
+ 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.`
1420
+ });
1421
+ }
1275
1422
  const ready = checks.every((c) => c.ok);
1276
1423
  return { ready, checks, nextSteps: buildNextSteps(checks, ready) };
1277
1424
  }
@@ -1287,7 +1434,13 @@ function buildNextSteps(checks, ready) {
1287
1434
  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
1435
  }
1289
1436
  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.");
1437
+ steps.push(
1438
+ 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."
1439
+ );
1440
+ }
1441
+ const identity = by("panelIdentity");
1442
+ if (identity && identity.ok === false) {
1443
+ steps.push(identity.fix);
1291
1444
  }
1292
1445
  if (by("afterEffectsRunning")?.ok === false) {
1293
1446
  steps.push("Open After Effects 2026.");
@@ -1302,38 +1455,15 @@ function buildNextSteps(checks, ready) {
1302
1455
  }
1303
1456
 
1304
1457
  // src/setup/install.ts
1305
- import { execFile as execFile2 } from "node:child_process";
1306
1458
  import fs5 from "node:fs";
1307
1459
  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
1460
  async function installPanel(opts = {}) {
1333
1461
  const actions = [];
1334
1462
  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.");
1463
+ if (!isSupportedPlatform()) {
1464
+ throw new Error(
1465
+ `After Effects runs only on macOS and Windows, so setup_panel cannot install anything on ${process.platform}.`
1466
+ );
1337
1467
  }
1338
1468
  const source = panelSourceDir();
1339
1469
  if (!source) {
@@ -1377,16 +1507,16 @@ async function installPanel(opts = {}) {
1377
1507
  }
1378
1508
  let rebootRecommended = false;
1379
1509
  if (opts.enableDebugMode !== false) {
1380
- const wasOn = await isDebugModeOn();
1381
- if (wasOn) {
1382
- actions.push("Adobe's PlayerDebugMode preference was already enabled.");
1510
+ const existing2 = await isDebugModeOn();
1511
+ if (existing2.on) {
1512
+ actions.push(`PlayerDebugMode was already enabled \u2014 ${existing2.detail}.`);
1383
1513
  } else {
1384
1514
  const versions = await enableDebugMode();
1385
1515
  if (versions.length === 0) {
1386
- notes.push("Failed to set PlayerDebugMode. Without it After Effects will refuse to load this panel.");
1516
+ notes.push(`Failed to set ${debugModeLocation()}. Without it After Effects will refuse to load this panel.`);
1387
1517
  } else {
1388
1518
  actions.push(`Enabled PlayerDebugMode for CSXS ${versions.join(", ")} so AE will load the unsigned panel.`);
1389
- rebootRecommended = true;
1519
+ rebootRecommended = process.platform === "darwin";
1390
1520
  }
1391
1521
  }
1392
1522
  }
@@ -1403,6 +1533,208 @@ async function installPanel(opts = {}) {
1403
1533
  };
1404
1534
  }
1405
1535
 
1536
+ // src/issues/journal.ts
1537
+ import fs6 from "node:fs";
1538
+ import os3 from "node:os";
1539
+ import path6 from "node:path";
1540
+ var REPO = "Engine-Room-Games/after-effects-mcp";
1541
+ var NEW_ISSUE_URL = `https://github.com/${REPO}/issues/new`;
1542
+ var SECTION_SYMPTOM = "What went wrong";
1543
+ var SECTION_CAUSE = "Why";
1544
+ var SECTION_WORKAROUND = "What worked";
1545
+ function journalRoot() {
1546
+ const override = process.env.AE_MCP_HOME?.trim();
1547
+ if (override && override.length > 0) return { dir: override, scope: "project" };
1548
+ const cwd = process.cwd();
1549
+ const unusable = cwd === path6.parse(cwd).root || cwd === os3.homedir();
1550
+ if (!unusable) {
1551
+ try {
1552
+ fs6.accessSync(cwd, fs6.constants.W_OK);
1553
+ return { dir: path6.join(cwd, ".ae-mcp"), scope: "project" };
1554
+ } catch {
1555
+ }
1556
+ }
1557
+ return { dir: path6.join(os3.homedir(), ".after-effects-mcp"), scope: "home" };
1558
+ }
1559
+ function journalDir() {
1560
+ return path6.join(journalRoot().dir, "issues");
1561
+ }
1562
+ function ensureJournalDir() {
1563
+ const { dir } = journalRoot();
1564
+ const issues = path6.join(dir, "issues");
1565
+ fs6.mkdirSync(issues, { recursive: true });
1566
+ const ignore = path6.join(dir, ".gitignore");
1567
+ if (!fs6.existsSync(ignore)) fs6.writeFileSync(ignore, "*\n", "utf8");
1568
+ return issues;
1569
+ }
1570
+ function slugify(text) {
1571
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
1572
+ return slug.length > 0 ? slug : `issue-${Date.now()}`;
1573
+ }
1574
+ function today() {
1575
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1576
+ }
1577
+ function oneLine(text) {
1578
+ return text.replace(/\s+/g, " ").trim();
1579
+ }
1580
+ function entryPath(id) {
1581
+ const dir = path6.resolve(journalDir());
1582
+ const file = path6.resolve(dir, `${id}.md`);
1583
+ if (path6.dirname(file) !== dir) throw new Error(`Invalid issue id: ${id}`);
1584
+ return file;
1585
+ }
1586
+ function render(entry) {
1587
+ const lines = [
1588
+ "---",
1589
+ `id: ${entry.id}`,
1590
+ `title: ${oneLine(entry.title)}`,
1591
+ `tools: ${entry.tools.join(", ")}`,
1592
+ `firstSeen: ${entry.firstSeen}`,
1593
+ `lastSeen: ${entry.lastSeen}`,
1594
+ `occurrences: ${entry.occurrences}`,
1595
+ `reported: ${entry.reported}`,
1596
+ `issueUrl: ${entry.issueUrl ?? ""}`,
1597
+ "---",
1598
+ "",
1599
+ `## ${SECTION_SYMPTOM}`,
1600
+ "",
1601
+ entry.symptom.trim(),
1602
+ ""
1603
+ ];
1604
+ if (entry.cause && entry.cause.trim().length > 0) {
1605
+ lines.push(`## ${SECTION_CAUSE}`, "", entry.cause.trim(), "");
1606
+ }
1607
+ lines.push(`## ${SECTION_WORKAROUND}`, "", entry.workaround.trim(), "");
1608
+ return lines.join("\n");
1609
+ }
1610
+ function readSections(body) {
1611
+ const marks = [];
1612
+ for (const heading of [SECTION_SYMPTOM, SECTION_CAUSE, SECTION_WORKAROUND]) {
1613
+ const m = new RegExp(`^##[ \\t]+${heading}[ \\t]*$`, "im").exec(body);
1614
+ if (m) marks.push({ key: heading.toLowerCase(), from: m.index, to: m.index + m[0].length });
1615
+ }
1616
+ marks.sort((a, b) => a.from - b.from);
1617
+ const sections = /* @__PURE__ */ new Map();
1618
+ marks.forEach((mark, i) => {
1619
+ const end = i + 1 < marks.length ? marks[i + 1].from : body.length;
1620
+ sections.set(mark.key, body.slice(mark.to, end).trim());
1621
+ });
1622
+ return sections;
1623
+ }
1624
+ function parse(text, fallbackId) {
1625
+ const meta = {};
1626
+ let body = text;
1627
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
1628
+ if (fm) {
1629
+ for (const line of fm[1].split(/\r?\n/)) {
1630
+ const sep = line.indexOf(":");
1631
+ if (sep <= 0) continue;
1632
+ meta[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
1633
+ }
1634
+ body = text.slice(fm[0].length);
1635
+ }
1636
+ const sections = readSections(body);
1637
+ const occurrences = Number.parseInt(meta.occurrences ?? "1", 10);
1638
+ return {
1639
+ id: meta.id || fallbackId,
1640
+ title: meta.title || fallbackId.replace(/-/g, " "),
1641
+ tools: (meta.tools ?? "").split(",").map((t) => t.trim()).filter((t) => t.length > 0),
1642
+ firstSeen: meta.firstSeen || "",
1643
+ lastSeen: meta.lastSeen || meta.firstSeen || "",
1644
+ occurrences: Number.isFinite(occurrences) && occurrences > 0 ? occurrences : 1,
1645
+ reported: meta.reported === "true",
1646
+ issueUrl: meta.issueUrl && meta.issueUrl.length > 0 ? meta.issueUrl : void 0,
1647
+ // A hand-edited file with no recognised headings still has its text kept,
1648
+ // rather than being silently reduced to an empty entry.
1649
+ symptom: sections.get(SECTION_SYMPTOM.toLowerCase()) ?? (sections.size === 0 ? body.trim() : ""),
1650
+ cause: sections.get(SECTION_CAUSE.toLowerCase()) || void 0,
1651
+ workaround: sections.get(SECTION_WORKAROUND.toLowerCase()) ?? ""
1652
+ };
1653
+ }
1654
+ function readEntry(file) {
1655
+ try {
1656
+ return parse(fs6.readFileSync(file, "utf8"), path6.basename(file, ".md"));
1657
+ } catch {
1658
+ return null;
1659
+ }
1660
+ }
1661
+ function logIssue(input) {
1662
+ const id = slugify(input.title);
1663
+ const file = entryPath(id);
1664
+ const existing = fs6.existsSync(file) ? readEntry(file) : null;
1665
+ const entry = {
1666
+ id,
1667
+ title: oneLine(input.title),
1668
+ tools: input.tools ?? existing?.tools ?? [],
1669
+ firstSeen: existing?.firstSeen || today(),
1670
+ lastSeen: today(),
1671
+ // Repeats are worth counting: an entry seen five times is the one most
1672
+ // worth reporting, and the count is the only evidence of that.
1673
+ occurrences: (existing?.occurrences ?? 0) + 1,
1674
+ // Reporting state belongs to the entry, not to this sighting — a fresh
1675
+ // description of a known problem must not un-report it.
1676
+ reported: existing?.reported ?? false,
1677
+ issueUrl: existing?.issueUrl,
1678
+ symptom: input.symptom,
1679
+ // A cause worked out once is not lost because a later sighting was logged
1680
+ // without one.
1681
+ cause: input.cause ?? existing?.cause,
1682
+ workaround: input.workaround
1683
+ };
1684
+ ensureJournalDir();
1685
+ fs6.writeFileSync(file, render(entry), "utf8");
1686
+ return {
1687
+ id,
1688
+ path: file,
1689
+ occurrences: entry.occurrences,
1690
+ previouslyLogged: existing !== null,
1691
+ reported: entry.reported,
1692
+ issueUrl: entry.issueUrl
1693
+ };
1694
+ }
1695
+ function listIssues(status = "all", tool) {
1696
+ const { scope } = journalRoot();
1697
+ const dir = journalDir();
1698
+ let entries = [];
1699
+ try {
1700
+ entries = fs6.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => readEntry(path6.join(dir, f))).filter((e) => e !== null);
1701
+ } catch {
1702
+ entries = [];
1703
+ }
1704
+ const wanted = tool?.trim().toLowerCase();
1705
+ const filtered = entries.filter((e) => {
1706
+ const byStatus = status === "all" ? true : status === "reported" ? e.reported : !e.reported;
1707
+ if (!byStatus) return false;
1708
+ if (!wanted) return true;
1709
+ return e.tools.some((t) => t.toLowerCase() === wanted) || e.title.toLowerCase().includes(wanted);
1710
+ });
1711
+ filtered.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen) || b.occurrences - a.occurrences);
1712
+ return {
1713
+ dir,
1714
+ scope,
1715
+ repo: REPO,
1716
+ newIssueUrl: NEW_ISSUE_URL,
1717
+ serverVersion: packageVersion(),
1718
+ platform: process.platform,
1719
+ count: filtered.length,
1720
+ issues: filtered
1721
+ };
1722
+ }
1723
+ function markReported(id, url) {
1724
+ const file = entryPath(slugify(id));
1725
+ const entry = fs6.existsSync(file) ? readEntry(file) : null;
1726
+ if (!entry) {
1727
+ const known = listIssues("all").issues.map((e) => e.id);
1728
+ throw new Error(
1729
+ `No journal entry with id "${id}".` + (known.length > 0 ? ` Known ids: ${known.join(", ")}` : "")
1730
+ );
1731
+ }
1732
+ entry.reported = true;
1733
+ if (url) entry.issueUrl = oneLine(url);
1734
+ fs6.writeFileSync(file, render(entry), "utf8");
1735
+ return entry;
1736
+ }
1737
+
1406
1738
  // src/util/pngImage.ts
1407
1739
  function imageContent(meta, base64) {
1408
1740
  return {
@@ -1417,13 +1749,22 @@ function imageContent(meta, base64) {
1417
1749
  var { OpSchemas: OpSchemas2 } = schemas_exports;
1418
1750
  var VISION_OPS = /* @__PURE__ */ new Set(["screenshot_frame", "screenshot_layer"]);
1419
1751
  var ASYNC_OPS = /* @__PURE__ */ new Set(["run_batch"]);
1420
- var SERVER_OPS = /* @__PURE__ */ new Set(["await_job", "get_job", "cancel_job", "check_setup", "setup_panel"]);
1752
+ var SERVER_OPS = /* @__PURE__ */ new Set([
1753
+ "await_job",
1754
+ "get_job",
1755
+ "cancel_job",
1756
+ "check_setup",
1757
+ "setup_panel",
1758
+ "log_issue",
1759
+ "list_known_issues",
1760
+ "mark_issue_reported"
1761
+ ]);
1421
1762
  var AwaitJobSchema = schemas_exports.AwaitJob;
1422
1763
  var GetJobSchema = schemas_exports.GetJob;
1423
1764
  var CancelJobSchema = schemas_exports.CancelJob;
1424
1765
  function createServer() {
1425
1766
  const server = new Server(
1426
- { name: "after-effects-mcp", version: "0.1.0" },
1767
+ { name: "after-effects-mcp", version: "0.1.2" },
1427
1768
  { capabilities: { tools: {}, logging: {} } }
1428
1769
  );
1429
1770
  const bridge = new HttpClient();
@@ -1482,6 +1823,19 @@ function createServer() {
1482
1823
  const installed = await installPanel({ enableDebugMode: a.enableDebugMode, force: a.force });
1483
1824
  return textResult({ ...installed, setup: await checkSetup() });
1484
1825
  }
1826
+ if (name === "log_issue") {
1827
+ const a = schemas_exports.LogIssue.parse(rawArgs);
1828
+ return textResult(logIssue(a));
1829
+ }
1830
+ if (name === "list_known_issues") {
1831
+ const a = schemas_exports.ListKnownIssues.parse(rawArgs);
1832
+ return textResult(listIssues(a.status ?? "all", a.tool));
1833
+ }
1834
+ if (name === "mark_issue_reported") {
1835
+ const a = schemas_exports.MarkIssueReported.parse(rawArgs);
1836
+ const entry = markReported(a.id, a.url);
1837
+ return textResult({ ok: true, id: entry.id, reported: true, issueUrl: entry.issueUrl });
1838
+ }
1485
1839
  } catch (e) {
1486
1840
  return errorResult(e.message);
1487
1841
  }
@@ -1578,11 +1932,12 @@ client (Claude Code, Claude Desktop, \u2026) starts it. You do not normally run
1578
1932
  by hand.
1579
1933
 
1580
1934
  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.
1935
+ init <directory> [--no-mcp] Create a project folder containing the client
1936
+ configuration and a house-style skill you can
1937
+ edit. Pass --no-mcp if your client already
1938
+ provides these tools.
1939
+ --help Show this message.
1940
+ --version Print the version.
1586
1941
  `;
1587
1942
  async function main() {
1588
1943
  const argv = process.argv.slice(2);
@@ -1609,7 +1964,7 @@ ${USAGE}`);
1609
1964
  await server.connect(transport);
1610
1965
  logger.info("MCP server running on stdio");
1611
1966
  }
1612
- var VERSION = "0.1.0";
1967
+ var VERSION = "0.1.2";
1613
1968
  main().catch((e) => {
1614
1969
  logger.error("fatal", e.message);
1615
1970
  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.2",
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.2"
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.2" />
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-11T09:11:46.741Z
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.2",
4
4
  "private": true,
5
5
  "description": "Invisible CEP extension hosting an HTTP+WS bridge inside After Effects.",
6
6
  "dependencies": {