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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/server.js CHANGED
@@ -9,210 +9,277 @@ var __export = (target, all) => {
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
10
 
11
11
  // src/cli/init.ts
12
+ import path2 from "node:path";
13
+
14
+ // src/setup/scaffold.ts
12
15
  import fs from "node:fs";
16
+ import os from "node:os";
13
17
  import path from "node:path";
14
- var HOUSE_STYLE = `---
15
- name: house-style
16
- description: The visual and motion style for this project \u2014 palette, type, timing and layout rules. Load whenever building or editing anything in After Effects for this project.
17
- ---
18
-
19
- # House style
20
-
21
- <!--
22
- This file is yours. Claude reads it before building anything in After Effects,
23
- so whatever you write here becomes the default look of your work.
24
-
25
- Replace the placeholders below with your own values. Be specific and concrete:
26
- "dark navy #131521 at 92% opacity" is usable, "modern and clean" is not. If a
27
- number matters \u2014 a corner radius, a stroke width, a hold duration \u2014 write the
28
- number down.
29
-
30
- The fastest way to fill this in: build one piece the way you like it, then ask
31
- Claude "read this comp and write it up in my house-style skill".
32
- -->
33
-
34
- ## Palette
35
-
36
- | Role | Colour | Notes |
37
- |---|---|---|
38
- | Background | \`#000000\` | |
39
- | Primary text | \`#FFFFFF\` | |
40
- | Accent | \`#3DC46E\` | Used for emphasis and positive values |
41
- | Warning / negative | \`#E03333\` | |
42
-
43
- ## Type
44
-
45
- - **Headings:** _font name_, weight, size range
46
- - **Body:** _font name_, weight, size range
47
- - **Tracking / leading:** _your defaults_
48
- - Text is left-aligned unless stated otherwise.
49
-
50
- ## Motion
51
-
52
- - **Standard in:** scale 0 \u2192 108 \u2192 100 with easy ease, over ~0.4s
53
- - **Standard out:** scale \u2192 0 over ~0.3s
54
- - **Easing:** easy ease on everything; no linear motion unless mechanical
55
- - **Idle life:** subtle \`wiggle()\` on position so nothing sits perfectly still
56
-
57
- ## Layout
58
-
59
- - Comp size and frame rate: _e.g. 1920\xD71080 at 30fps_
60
- - Safe margins: _e.g. 120px from every edge_
61
- - Where elements sit by default: _e.g. lower third, left-aligned_
62
-
63
- ## Rules
64
-
65
- - _Anything that should always or never happen. E.g. "never put text directly on
66
- footage \u2014 always on a rounded chip", "keep total runtime under 8 seconds"._
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
- `;
101
- var CLAUDE_MD = `# {{NAME}}
18
+ var ScaffoldError = class extends Error {
19
+ };
20
+ function detectClient(clientName) {
21
+ const n = (clientName ?? "").toLowerCase();
22
+ if (!n) return "generic";
23
+ if (n.includes("claude-code") || n.includes("claude code")) return "claude-code";
24
+ if (n.includes("claude-ai") || n.includes("claude desktop")) return "claude-desktop";
25
+ if (n.includes("cursor")) return "cursor";
26
+ if (n.includes("windsurf") || n.includes("codeium")) return "windsurf";
27
+ if (n.includes("codex")) return "codex";
28
+ if (n.includes("visual studio code") || n.includes("vscode") || n.includes("copilot")) return "vscode";
29
+ return "generic";
30
+ }
31
+ var MCP_SERVER_ENTRY = {
32
+ command: "npx",
33
+ args: ["-y", "@engine-room/after-effects-mcp"]
34
+ };
35
+ function mcpConfigFor(client) {
36
+ const standard = JSON.stringify({ mcpServers: { "after-effects": MCP_SERVER_ENTRY } }, null, 2) + "\n";
37
+ switch (client) {
38
+ case "claude-code":
39
+ return { rel: ".mcp.json", json: standard };
40
+ case "cursor":
41
+ return { rel: path.join(".cursor", "mcp.json"), json: standard };
42
+ case "vscode":
43
+ return {
44
+ rel: path.join(".vscode", "mcp.json"),
45
+ json: JSON.stringify({ servers: { "after-effects": { type: "stdio", ...MCP_SERVER_ENTRY } } }, null, 2) + "\n"
46
+ };
47
+ default:
48
+ return void 0;
49
+ }
50
+ }
51
+ function globalConfigHint(client) {
52
+ const standard = JSON.stringify({ mcpServers: { "after-effects": MCP_SERVER_ENTRY } }, null, 2) + "\n";
53
+ const home = os.homedir();
54
+ switch (client) {
55
+ case "claude-desktop":
56
+ return {
57
+ path: process.platform === "darwin" ? path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json"),
58
+ json: standard
59
+ };
60
+ case "windsurf":
61
+ return { path: path.join(home, ".codeium", "windsurf", "mcp_config.json"), json: standard };
62
+ case "codex":
63
+ return {
64
+ path: path.join(home, ".codex", "config.toml"),
65
+ json: '[mcp_servers.after-effects]\ncommand = "npx"\nargs = ["-y", "@engine-room/after-effects-mcp"]\n'
66
+ };
67
+ default:
68
+ return void 0;
69
+ }
70
+ }
71
+ var AGENTS_MD = (name) => `# ${name}
102
72
 
103
- After Effects project folder. The tools drive After Effects directly from here.
73
+ An After Effects project folder. The AE tools drive After Effects directly from
74
+ here \u2014 describe what you want and it gets built in the open project.
104
75
 
105
76
  ## How to work in this folder
106
77
 
107
78
  1. Open After Effects with the project you want to work on.
108
- 2. Describe what you want in plain language \u2014 "build a lower third that says
109
- Chapter One and slides in from the left".
79
+ 2. Say what you want in plain language \u2014 "build a lower third that says Chapter
80
+ One and slides in from the left".
110
81
  3. The current state of the comp is read, the change is made, and you see it.
111
82
 
112
83
  ## Style
113
84
 
114
- The look of everything built here is defined in
115
- \`.claude/skills/house-style/SKILL.md\`. Edit that file to change the defaults \u2014
116
- palette, type, timing, layout. It is read automatically.
85
+ The look of everything built here comes from \`house-style.md\`, which sits next
86
+ to the After Effects project file itself. Ask for a style guide and one gets
87
+ written from a comp you already like; edit it in any text editor afterwards.
88
+
89
+ It travels with the .aep, so it applies wherever the project is opened.
117
90
 
118
91
  ## When a tool misbehaves
119
92
 
120
93
  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.
94
+ solved it. Anything newly worked out goes in with \`log_issue\`, and the
95
+ report-ae-issue prompt sends it to the maintainers.
125
96
 
126
97
  ## Conventions for this project
127
98
 
128
- <!-- Anything specific to this project rather than to your general style:
129
- naming conventions for comps and layers, delivery specs, the client's
99
+ <!-- Anything specific to this project rather than to your general style: naming
100
+ conventions for comps and layers, delivery specs, the client's
130
101
  requirements, what lives in which comp. -->
131
102
 
132
103
  - Renders go in \`renders/\`.
133
104
  `;
134
- var MCP_JSON = `{
135
- "mcpServers": {
136
- "after-effects": {
137
- "command": "npx",
138
- "args": ["-y", "@engine-room/after-effects-mcp"]
139
- }
105
+ function pointerContent(rel) {
106
+ const body = `# After Effects project
107
+
108
+ See [AGENTS.md](AGENTS.md) for how this folder works, and \`house-style.md\`
109
+ beside the .aep for the look everything should follow.
110
+ `;
111
+ if (rel.endsWith(".mdc")) {
112
+ return `---
113
+ description: How this After Effects project folder works
114
+ alwaysApply: true
115
+ ---
116
+
117
+ ${body}`;
140
118
  }
119
+ return body;
141
120
  }
142
- `;
143
- var KNOWN_FLAGS = ["--no-mcp", "--with-mcp"];
121
+ function pointerFiles(client) {
122
+ switch (client) {
123
+ case "claude-code":
124
+ return ["CLAUDE.md"];
125
+ case "cursor":
126
+ return [path.join(".cursor", "rules", "after-effects.mdc")];
127
+ case "windsurf":
128
+ return [".windsurfrules"];
129
+ case "vscode":
130
+ return [path.join(".github", "copilot-instructions.md")];
131
+ default:
132
+ return [];
133
+ }
134
+ }
135
+ function resolveTarget(dir, roots) {
136
+ if (dir && dir.trim().length > 0) {
137
+ return { dir: path.resolve(dir.trim()), resolvedFrom: "argument" };
138
+ }
139
+ const root = roots?.find((r) => r && r.trim().length > 0);
140
+ if (root) return { dir: path.resolve(root), resolvedFrom: "client-root" };
141
+ const cwd = process.cwd();
142
+ const isFilesystemRoot = cwd === path.parse(cwd).root;
143
+ if (isFilesystemRoot || cwd === os.homedir()) {
144
+ throw new ScaffoldError(
145
+ `No project folder to write to. This client did not say which folder it is working in, and the server was started in ${cwd}, which is not somewhere a project should be created. Ask the user which folder they want the project in \u2014 a new one is fine \u2014 and pass it as \`dir\`.`
146
+ );
147
+ }
148
+ return { dir: cwd, resolvedFrom: "working-directory" };
149
+ }
150
+ function scaffold(opts) {
151
+ const { dir, resolvedFrom } = resolveTarget(opts.dir, opts.roots);
152
+ const name = opts.name?.trim() || path.basename(dir);
153
+ const files = [["AGENTS.md", AGENTS_MD(name)]];
154
+ for (const rel of pointerFiles(opts.client)) files.push([rel, pointerContent(rel)]);
155
+ files.push([path.join("renders", ".gitkeep"), ""]);
156
+ const projectConfig = opts.withMcpConfig ? mcpConfigFor(opts.client) : void 0;
157
+ if (projectConfig) files.push([projectConfig.rel, projectConfig.json]);
158
+ const existing = files.map(([rel]) => rel).filter((rel) => fs.existsSync(path.join(dir, rel)));
159
+ if (existing.length > 0) {
160
+ throw new ScaffoldError(
161
+ `${dir} already has ${existing.join(", ")}. Nothing was written. This folder is already set up \u2014 or pick a different one.`
162
+ );
163
+ }
164
+ for (const [rel, content] of files) {
165
+ const full = path.join(dir, rel);
166
+ fs.mkdirSync(path.dirname(full), { recursive: true });
167
+ fs.writeFileSync(full, content, "utf8");
168
+ }
169
+ const hint = opts.withMcpConfig && !projectConfig ? globalConfigHint(opts.client) : void 0;
170
+ const nextSteps = [
171
+ "Open After Effects and open (or save) the project you want to work on.",
172
+ "Ask to set up After Effects if the tools cannot reach it yet \u2014 that installs the panel, once per machine.",
173
+ "Ask for a style guide, pointing at a comp that already looks the way you want. It is saved next to the .aep."
174
+ ];
175
+ if (hint) nextSteps.unshift(`Add the After Effects server to ${hint.path}, then restart the app.`);
176
+ return {
177
+ dir,
178
+ name,
179
+ client: opts.client,
180
+ written: files.map(([rel]) => rel),
181
+ resolvedFrom,
182
+ mcpConfigHint: hint,
183
+ nextSteps
184
+ };
185
+ }
186
+
187
+ // src/cli/init.ts
188
+ var CLIENTS = [
189
+ "claude-code",
190
+ "claude-desktop",
191
+ "cursor",
192
+ "vscode",
193
+ "windsurf",
194
+ "codex",
195
+ "generic"
196
+ ];
144
197
  function parseInitArgs(argv) {
145
- const positional = argv.filter((a) => !a.startsWith("-"));
146
- const withMcp = !argv.includes("--no-mcp");
147
- const unknown = argv.filter((a) => a.startsWith("-") && !KNOWN_FLAGS.includes(a));
148
- if (unknown.length > 0) return { error: `Unknown option: ${unknown[0]}` };
198
+ const positional = [];
199
+ let withMcp = true;
200
+ let client = "claude-code";
201
+ for (let i = 0; i < argv.length; i++) {
202
+ const arg = argv[i];
203
+ if (arg === "--no-mcp") withMcp = false;
204
+ else if (arg === "--with-mcp") withMcp = true;
205
+ else if (arg === "--client" || arg.startsWith("--client=")) {
206
+ const value = arg.startsWith("--client=") ? arg.slice("--client=".length) : argv[++i];
207
+ if (!value) return { error: "--client needs a value." };
208
+ if (!CLIENTS.includes(value)) {
209
+ return { error: `Unknown client "${value}". One of: ${CLIENTS.join(", ")}.` };
210
+ }
211
+ client = value;
212
+ } else if (arg.startsWith("-")) {
213
+ return { error: `Unknown option: ${arg}` };
214
+ } else positional.push(arg);
215
+ }
149
216
  if (positional.length === 0) return { error: "Missing target directory." };
150
217
  if (positional.length > 1) return { error: `Expected one directory, got ${positional.length}.` };
151
- return { dir: positional[0], withMcp };
218
+ return { dir: positional[0], withMcp, client };
152
219
  }
153
220
  function runInit(argv) {
154
221
  const parsed = parseInitArgs(argv);
155
222
  if ("error" in parsed) {
156
- process.stderr.write(`${parsed.error}
157
-
158
- Usage: npx @engine-room/after-effects-mcp init <directory> [--no-mcp]
159
- `);
160
- return 1;
161
- }
162
- const target = path.resolve(parsed.dir);
163
- const name = path.basename(target);
164
- const files = [
165
- ["CLAUDE.md", CLAUDE_MD.replace("{{NAME}}", name)],
166
- [path.join(".claude", "skills", "house-style", "SKILL.md"), HOUSE_STYLE],
167
- [path.join(".claude", "commands", "report-ae-issue.md"), REPORT_COMMAND],
168
- [path.join("renders", ".gitkeep"), ""]
169
- ];
170
- if (parsed.withMcp) files.push([".mcp.json", MCP_JSON]);
171
- const existing = files.map(([rel]) => rel).filter((rel) => fs.existsSync(path.join(target, rel)));
172
- if (existing.length > 0) {
173
223
  process.stderr.write(
174
- `Refusing to overwrite existing files in ${target}:
175
- ` + existing.map((f) => ` ${f}
176
- `).join("") + `
177
- Delete them first, or choose a different directory.
224
+ `${parsed.error}
225
+
226
+ Usage: npx @engine-room/after-effects-mcp init <directory> [--no-mcp] [--client <name>]
227
+ clients: ${CLIENTS.join(", ")} (default claude-code)
178
228
  `
179
229
  );
180
230
  return 1;
181
231
  }
182
- for (const [rel, content] of files) {
183
- const full = path.join(target, rel);
184
- fs.mkdirSync(path.dirname(full), { recursive: true });
185
- fs.writeFileSync(full, content, "utf8");
186
- }
187
- const out = [
188
- `Created ${target}`,
189
- ``,
190
- ` CLAUDE.md what this project is`,
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`,
193
- ` renders/ exports land here`,
194
- ...parsed.withMcp ? [` .mcp.json connects your client to After Effects`] : [],
195
- ``,
196
- `Next:`,
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.`,
200
- ...parsed.withMcp ? [] : [
232
+ let result;
233
+ try {
234
+ result = scaffold({
235
+ dir: parsed.dir,
236
+ client: parsed.client,
237
+ withMcpConfig: parsed.withMcp
238
+ });
239
+ } catch (e) {
240
+ if (e instanceof ScaffoldError) {
241
+ process.stderr.write(`${e.message}
242
+ `);
243
+ return 1;
244
+ }
245
+ throw e;
246
+ }
247
+ const lines = [`Created ${result.dir}`, ``];
248
+ for (const rel of result.written) {
249
+ if (rel.endsWith(".gitkeep")) lines.push(` ${pad(path2.dirname(rel) + "/")} exports land here`);
250
+ else if (rel === "AGENTS.md") lines.push(` ${pad(rel)} what this project is`);
251
+ else if (rel.endsWith("mcp.json")) lines.push(` ${pad(rel)} connects your client to After Effects`);
252
+ else lines.push(` ${pad(rel)} points your client at AGENTS.md`);
253
+ }
254
+ lines.push(``, `Next:`);
255
+ lines.push(` 1. Open the folder in your AI client: cd ${parsed.dir}`);
256
+ result.nextSteps.forEach((s, i) => lines.push(` ${i + 2}. ${s}`));
257
+ if (result.mcpConfigHint) {
258
+ lines.push(
201
259
  ``,
202
- `This folder has no .mcp.json, so your client must already provide the`,
203
- `After Effects tools some other way.`
204
- ],
205
- ``
206
- ].join("\n");
207
- process.stdout.write(out);
260
+ `${parsed.client} configures servers globally rather than per folder. Add this to`,
261
+ `${result.mcpConfigHint.path}:`,
262
+ ``,
263
+ result.mcpConfigHint.json.trimEnd()
264
+ );
265
+ }
266
+ lines.push(``);
267
+ process.stdout.write(lines.join("\n"));
208
268
  return 0;
209
269
  }
270
+ function pad(s) {
271
+ return s.padEnd(38);
272
+ }
210
273
 
211
274
  // src/server.ts
212
275
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
213
276
  import {
214
277
  CallToolRequestSchema,
215
- ListToolsRequestSchema
278
+ GetPromptRequestSchema,
279
+ ListPromptsRequestSchema,
280
+ ListResourcesRequestSchema,
281
+ ListToolsRequestSchema,
282
+ ReadResourceRequestSchema
216
283
  } from "@modelcontextprotocol/sdk/types.js";
217
284
  import { zodToJsonSchema } from "zod-to-json-schema";
218
285
 
@@ -275,6 +342,7 @@ __export(schemas_exports, {
275
342
  AddMask: () => AddMask,
276
343
  AddShapeContent: () => AddShapeContent,
277
344
  AddTextAnimator: () => AddTextAnimator,
345
+ AeGuide: () => AeGuide,
278
346
  AwaitJob: () => AwaitJob,
279
347
  CancelJob: () => CancelJob,
280
348
  CheckSetup: () => CheckSetup,
@@ -294,13 +362,16 @@ __export(schemas_exports, {
294
362
  DeleteLayer: () => DeleteLayer,
295
363
  DuplicateLayer: () => DuplicateLayer,
296
364
  FindLayers: () => FindLayers,
365
+ GUIDE_TOPICS: () => GUIDE_TOPICS,
297
366
  GetComp: () => GetComp,
298
367
  GetCompTree: () => GetCompTree,
299
368
  GetExpression: () => GetExpression,
369
+ GetHouseStyle: () => GetHouseStyle,
300
370
  GetJob: () => GetJob,
301
371
  GetKeyframes: () => GetKeyframes,
302
372
  GetLayerFull: () => GetLayerFull,
303
373
  GetProjectSummary: () => GetProjectSummary,
374
+ InitProject: () => InitProject,
304
375
  Interpolation: () => Interpolation,
305
376
  ListAvailableEffects: () => ListAvailableEffects,
306
377
  ListComps: () => ListComps,
@@ -326,6 +397,7 @@ __export(schemas_exports, {
326
397
  SetEffectEnabled: () => SetEffectEnabled,
327
398
  SetEffectParam: () => SetEffectParam,
328
399
  SetExpression: () => SetExpression,
400
+ SetHouseStyle: () => SetHouseStyle,
329
401
  SetInterpolation: () => SetInterpolation,
330
402
  SetLayer: () => SetLayer,
331
403
  SetMask: () => SetMask,
@@ -710,11 +782,26 @@ var FindLayers = z2.object({
710
782
  hasEffectMatchName: z2.string().optional()
711
783
  });
712
784
  var RunJsx = z2.object({ code: z2.string() });
785
+ var GetHouseStyle = z2.object({}).strict();
786
+ var SetHouseStyle = z2.object({
787
+ content: z2.string().min(1).describe("The complete style guide as markdown. Replaces the file, so send the whole document."),
788
+ overwrite: z2.boolean().default(false).optional().describe("Required to replace an existing guide. Read it with get_house_style and merge first \u2014 this is not a patch.")
789
+ }).strict();
713
790
  var CheckSetup = z2.object({}).strict();
714
791
  var SetupPanel = z2.object({
715
792
  enableDebugMode: z2.boolean().default(true).optional().describe("Also enable Adobe's PlayerDebugMode preference, which AE requires to load this unsigned panel. Default true."),
716
793
  force: z2.boolean().default(false).optional().describe("Replace an existing symlinked (development) install with a copy. Default false.")
717
794
  }).strict();
795
+ var GUIDE_TOPICS = ["ae-setup", "after-effects", "style-guide"];
796
+ var AeGuide = z2.object({
797
+ topic: z2.enum(GUIDE_TOPICS).describe("after-effects: building, animating, easing, expressions, the traps. style-guide: capturing the user's look. ae-setup: connecting to AE when a tool cannot reach it.")
798
+ }).strict();
799
+ var InitProject = z2.object({
800
+ dir: z2.string().optional().describe("Folder to create or fill, absolute or relative to the server's working directory. Ask the user if you do not know; do not invent one."),
801
+ name: z2.string().optional().describe("Project name for the generated docs. Defaults to the folder name."),
802
+ client: z2.enum(["auto", "claude-code", "claude-desktop", "cursor", "vscode", "windsurf", "codex", "generic"]).default("auto").optional().describe("Which client's layout to write. 'auto' detects it from the MCP handshake \u2014 leave it alone unless the user says otherwise."),
803
+ withMcpConfig: z2.boolean().default(false).optional().describe("Also write a client MCP config pointing at this server. Default false: you are already connected, so the user does not need one.")
804
+ }).strict();
718
805
  var LogIssue = z2.object({
719
806
  title: z2.string().min(3).describe("One line naming the problem, specific enough to recognise again. Becomes the entry's id."),
720
807
  symptom: z2.string().min(3).describe("What went wrong, including the exact error text and the call that produced it."),
@@ -803,6 +890,9 @@ var OpSchemas = {
803
890
  find_layers: FindLayers,
804
891
  // raw
805
892
  run_jsx: RunJsx,
893
+ // house style
894
+ get_house_style: GetHouseStyle,
895
+ set_house_style: SetHouseStyle,
806
896
  // jobs
807
897
  await_job: AwaitJob,
808
898
  get_job: GetJob,
@@ -810,6 +900,9 @@ var OpSchemas = {
810
900
  // setup
811
901
  check_setup: CheckSetup,
812
902
  setup_panel: SetupPanel,
903
+ init_project: InitProject,
904
+ // guidance
905
+ ae_guide: AeGuide,
813
906
  // issue journal
814
907
  log_issue: LogIssue,
815
908
  list_known_issues: ListKnownIssues,
@@ -858,8 +951,8 @@ var logger = {
858
951
 
859
952
  // src/bridge/discovery.ts
860
953
  import fs2 from "node:fs";
861
- import os from "node:os";
862
- import path2 from "node:path";
954
+ import os2 from "node:os";
955
+ import path3 from "node:path";
863
956
  var DEFAULT_PORT = 7777;
864
957
  function discoverPort() {
865
958
  const envPort = process.env.AE_MCP_PORT;
@@ -868,7 +961,7 @@ function discoverPort() {
868
961
  if (Number.isFinite(n)) return n;
869
962
  }
870
963
  try {
871
- const f = path2.join(os.homedir(), ".engineroom-ae-mcp", "port");
964
+ const f = path3.join(os2.homedir(), ".engineroom-ae-mcp", "port");
872
965
  if (fs2.existsSync(f)) {
873
966
  const txt = fs2.readFileSync(f, "utf8").trim();
874
967
  const n = parseInt(txt, 10);
@@ -887,6 +980,8 @@ var HttpClient = class {
887
980
  this.port = port ?? discoverPort();
888
981
  this.base = `http://127.0.0.1:${this.port}`;
889
982
  }
983
+ // `bundleHash` is absent on panels installed before it was added; callers must
984
+ // treat undefined as "too old to say" rather than as a mismatch.
890
985
  async health() {
891
986
  try {
892
987
  const r = await fetch(`${this.base}/health`, { signal: AbortSignal.timeout(2e3) });
@@ -1165,13 +1260,19 @@ var descriptions = {
1165
1260
  find_layers: "Search across one or all comps for layers matching name/type/effect filters.",
1166
1261
  // ---------- raw ----------
1167
1262
  run_jsx: "Escape hatch: arbitrary ExtendScript in an undo group. `comp`/`app`/`OPS`/helpers in scope. Use `return X` to send a value back; complex AE objects are coerced to plain props.",
1263
+ // ---------- house style ----------
1264
+ get_house_style: "The user's palette, type, motion and layout defaults for the project that is open, read from `house-style.md` beside the .aep. Call it once before building anything so your work matches the rest of theirs. `found:false` means none exists yet \u2014 build with sensible defaults and offer to capture one afterwards. Cheap; never a reason to skip.",
1265
+ set_house_style: "Write the project's style guide. Replaces the whole file, so read it first and send the merged document \u2014 `overwrite:true` is required to replace an existing one. The project must have been saved at least once, since the file lives beside the .aep. Use the style-guide topic of ae_guide for how to capture a style worth writing down.",
1266
+ // ---------- guidance ----------
1267
+ ae_guide: "The full working guidance for these tools, by topic. Read `after-effects` before a first substantial build in a session, `style-guide` when capturing or editing the user's look, `ae-setup` when a tool cannot reach After Effects. Covers the traps that silently produce wrong output and are not visible from any single tool's schema.",
1168
1268
  // ---------- jobs ----------
1169
1269
  await_job: "Block until job is done. Default 10min timeout. Returns the same payload the tool would have.",
1170
1270
  get_job: "Non-blocking job status: progress/total/state/error.",
1171
1271
  cancel_job: "Set cancel flag; chunked loop stops at next boundary.",
1172
1272
  // ---------- setup ----------
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.",
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.",
1273
+ check_setup: "Diagnose the After Effects connection: panel installed, up to date, the version AE is actually running, 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 or says the panel is out of date, then relay `nextSteps` to the user in plain language. `panelRunningCurrent` is the one that predicts whether calls will work \u2014 it can fail while `panelUpToDate` passes, which means an update is installed but AE has not been restarted.",
1274
+ 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, out of date, or older than what AE is running. 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. Prefer running it while AE is CLOSED: the panel then loads when they open it, with no restart. If AE is already open they must quit and reopen it, and until they do, the old panel keeps answering. If the preference was newly enabled, a one-time Mac reboot may also be needed.",
1275
+ init_project: "Set up a working folder for one video, series or client: a project brief and a pointer to the house style, written in whichever layout this client reads. Run it when the user is starting out or asks to set up a project. It writes files to disk \u2014 say which folder before calling, and pass `dir` explicitly unless the client already told the server where it is working. It never overwrites anything and reports every path it wrote.",
1175
1276
  // ---------- issue journal ----------
1176
1277
  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
1278
  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.",
@@ -1179,14 +1280,14 @@ var descriptions = {
1179
1280
  };
1180
1281
 
1181
1282
  // src/setup/check.ts
1182
- import crypto from "node:crypto";
1183
- import fs4 from "node:fs";
1184
- import path4 from "node:path";
1283
+ import crypto2 from "node:crypto";
1284
+ import fs5 from "node:fs";
1285
+ import path6 from "node:path";
1185
1286
 
1186
1287
  // src/setup/paths.ts
1187
1288
  import fs3 from "node:fs";
1188
- import os2 from "node:os";
1189
- import path3 from "node:path";
1289
+ import os3 from "node:os";
1290
+ import path4 from "node:path";
1190
1291
  import { createRequire } from "node:module";
1191
1292
  import { fileURLToPath } from "node:url";
1192
1293
  var BUNDLE_ID = "games.engine-room.ae-mcp";
@@ -1194,56 +1295,64 @@ function isSupportedPlatform() {
1194
1295
  return process.platform === "darwin" || process.platform === "win32";
1195
1296
  }
1196
1297
  function packageRoot() {
1197
- let dir = path3.dirname(fileURLToPath(import.meta.url));
1298
+ let dir = path4.dirname(fileURLToPath(import.meta.url));
1198
1299
  for (let i = 0; i < 8; i++) {
1199
- if (fs3.existsSync(path3.join(dir, "package.json"))) return dir;
1200
- const parent = path3.dirname(dir);
1300
+ if (fs3.existsSync(path4.join(dir, "package.json"))) return dir;
1301
+ const parent = path4.dirname(dir);
1201
1302
  if (parent === dir) break;
1202
1303
  dir = parent;
1203
1304
  }
1204
- return path3.dirname(fileURLToPath(import.meta.url));
1305
+ return path4.dirname(fileURLToPath(import.meta.url));
1306
+ }
1307
+ function executableDir() {
1308
+ return path4.dirname(process.execPath);
1205
1309
  }
1206
1310
  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";
1311
+ for (const dir of [packageRoot(), executableDir()]) {
1312
+ try {
1313
+ const pkg = JSON.parse(fs3.readFileSync(path4.join(dir, "package.json"), "utf8"));
1314
+ if (typeof pkg.version === "string") return pkg.version;
1315
+ } catch {
1316
+ }
1212
1317
  }
1318
+ return "unknown";
1213
1319
  }
1214
1320
  function panelSourceDir() {
1215
1321
  const candidates = [
1216
1322
  // The live workspace copy comes first so a git checkout always installs
1217
1323
  // what the developer is editing, never a stale vendored copy left behind by
1218
1324
  // a previous `npm pack`. Only the second path exists in the tarball.
1219
- path3.resolve(packageRoot(), "..", "ae-panel"),
1220
- path3.join(packageRoot(), "panel")
1325
+ path4.resolve(packageRoot(), "..", "ae-panel"),
1326
+ path4.join(packageRoot(), "panel"),
1327
+ // Compiled single-file build: the panel ships beside the executable.
1328
+ path4.join(executableDir(), "panel")
1221
1329
  ];
1222
1330
  for (const dir of candidates) {
1223
- if (fs3.existsSync(path3.join(dir, "CSXS", "manifest.xml"))) return dir;
1331
+ if (fs3.existsSync(path4.join(dir, "CSXS", "manifest.xml"))) return dir;
1224
1332
  }
1225
1333
  return null;
1226
1334
  }
1227
1335
  function cepExtensionsDir() {
1228
1336
  if (process.platform === "win32") {
1229
- const appData = process.env.APPDATA ?? path3.join(os2.homedir(), "AppData", "Roaming");
1230
- return path3.join(appData, "Adobe", "CEP", "extensions");
1337
+ const appData = process.env.APPDATA ?? path4.join(os3.homedir(), "AppData", "Roaming");
1338
+ return path4.join(appData, "Adobe", "CEP", "extensions");
1231
1339
  }
1232
- return path3.join(os2.homedir(), "Library", "Application Support", "Adobe", "CEP", "extensions");
1340
+ return path4.join(os3.homedir(), "Library", "Application Support", "Adobe", "CEP", "extensions");
1233
1341
  }
1234
1342
  function installedPanelDir() {
1235
- return path3.join(cepExtensionsDir(), BUNDLE_ID);
1343
+ return path4.join(cepExtensionsDir(), BUNDLE_ID);
1236
1344
  }
1237
1345
  function wsModuleDir() {
1238
1346
  try {
1239
1347
  const require2 = createRequire(import.meta.url);
1240
1348
  const entry = require2.resolve("ws");
1241
- const marker = `${path3.sep}node_modules${path3.sep}ws${path3.sep}`;
1349
+ const marker = `${path4.sep}node_modules${path4.sep}ws${path4.sep}`;
1242
1350
  const idx = entry.lastIndexOf(marker);
1243
1351
  if (idx >= 0) return entry.slice(0, idx + marker.length - 1);
1244
- return path3.dirname(entry);
1352
+ return path4.dirname(entry);
1245
1353
  } catch {
1246
- return null;
1354
+ const beside = path4.join(executableDir(), "node_modules", "ws");
1355
+ return fs3.existsSync(beside) ? beside : null;
1247
1356
  }
1248
1357
  }
1249
1358
  function copyRecursive(src, dst) {
@@ -1251,7 +1360,7 @@ function copyRecursive(src, dst) {
1251
1360
  if (stat.isDirectory()) {
1252
1361
  fs3.mkdirSync(dst, { recursive: true });
1253
1362
  for (const entry of fs3.readdirSync(src)) {
1254
- copyRecursive(path3.join(src, entry), path3.join(dst, entry));
1363
+ copyRecursive(path4.join(src, entry), path4.join(dst, entry));
1255
1364
  }
1256
1365
  } else if (stat.isSymbolicLink()) {
1257
1366
  fs3.symlinkSync(fs3.readlinkSync(src), dst);
@@ -1260,6 +1369,56 @@ function copyRecursive(src, dst) {
1260
1369
  }
1261
1370
  }
1262
1371
 
1372
+ // src/setup/panelVersion.ts
1373
+ import crypto from "node:crypto";
1374
+ import fs4 from "node:fs";
1375
+ import path5 from "node:path";
1376
+ var cachedSourceHash;
1377
+ function sourceBundleHash() {
1378
+ if (cachedSourceHash !== void 0) return cachedSourceHash;
1379
+ const source = panelSourceDir();
1380
+ cachedSourceHash = source ? hashFile(path5.join(source, "jsx", "bundle.jsx")) : null;
1381
+ return cachedSourceHash;
1382
+ }
1383
+ function installedBundleHash(installedPanel) {
1384
+ return hashFile(path5.join(installedPanel, "jsx", "bundle.jsx"));
1385
+ }
1386
+ function hashFile(file) {
1387
+ try {
1388
+ return crypto.createHash("sha256").update(fs4.readFileSync(file)).digest("hex");
1389
+ } catch {
1390
+ return null;
1391
+ }
1392
+ }
1393
+ var STALE_PANEL_ADVICE = "Tell the user this in plain language, then do it: the After Effects panel is older than these tools and does not understand everything they can do now. Run setup_panel, then ask them to quit and reopen After Effects. Do not retry the failed call until they confirm it has restarted.";
1394
+ function assessPanel(runningHash, installedHash) {
1395
+ const shipped = sourceBundleHash();
1396
+ if (!shipped) return { state: "unknown", message: "" };
1397
+ if (runningHash === shipped) return { state: "current", message: "" };
1398
+ if (typeof runningHash !== "string" || runningHash.length === 0) {
1399
+ if (installedHash === shipped) {
1400
+ return {
1401
+ state: "restart-needed",
1402
+ message: "The After Effects panel has been updated on disk, but After Effects is still running the previous version. Ask the user to quit and reopen After Effects, then try again."
1403
+ };
1404
+ }
1405
+ return {
1406
+ state: "unknown",
1407
+ message: "The After Effects panel is too old to report its version, which means it predates these tools. " + STALE_PANEL_ADVICE
1408
+ };
1409
+ }
1410
+ if (installedHash === shipped) {
1411
+ return {
1412
+ state: "restart-needed",
1413
+ message: "The After Effects panel has been updated on disk, but After Effects is still running the previous version. Ask the user to quit and reopen After Effects, then try again. Running setup_panel again will not help \u2014 only a restart loads the new panel."
1414
+ };
1415
+ }
1416
+ return { state: "update-needed", message: `The After Effects panel is out of date. ${STALE_PANEL_ADVICE}` };
1417
+ }
1418
+ function unknownOpMessage(op) {
1419
+ return `The After Effects panel does not recognise "${op}". That always means the installed panel is older than these tools \u2014 this op did not exist when it was installed. ${STALE_PANEL_ADVICE}`;
1420
+ }
1421
+
1263
1422
  // src/setup/platform.ts
1264
1423
  import { execFile } from "node:child_process";
1265
1424
  import { promisify } from "node:util";
@@ -1340,7 +1499,7 @@ function debugModeLocation() {
1340
1499
  // src/setup/check.ts
1341
1500
  function sha256(file) {
1342
1501
  try {
1343
- return crypto.createHash("sha256").update(fs4.readFileSync(file)).digest("hex");
1502
+ return crypto2.createHash("sha256").update(fs5.readFileSync(file)).digest("hex");
1344
1503
  } catch {
1345
1504
  return null;
1346
1505
  }
@@ -1349,7 +1508,8 @@ async function bridgeReachable(port) {
1349
1508
  try {
1350
1509
  const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2e3) });
1351
1510
  if (!res.ok) return { ok: false, detail: `port ${port} returned HTTP ${res.status}` };
1352
- return { ok: true, detail: `responding on port ${port}` };
1511
+ const body = await res.json().catch(() => ({}));
1512
+ return { ok: true, detail: `responding on port ${port}`, bundleHash: body.bundleHash };
1353
1513
  } catch (e) {
1354
1514
  return { ok: false, detail: `no response on port ${port} (${e.message})` };
1355
1515
  }
@@ -1378,7 +1538,7 @@ async function checkSetup() {
1378
1538
  fix: debugMode.on ? void 0 : `Run the setup_panel tool. After Effects only loads unsigned panels when ${debugModeLocation()} is set.`
1379
1539
  });
1380
1540
  const installed = installedPanelDir();
1381
- const isInstalled = fs4.existsSync(path4.join(installed, "CSXS", "manifest.xml"));
1541
+ const isInstalled = fs5.existsSync(path6.join(installed, "CSXS", "manifest.xml"));
1382
1542
  checks.push({
1383
1543
  name: "panelInstalled",
1384
1544
  ok: isInstalled,
@@ -1386,8 +1546,8 @@ async function checkSetup() {
1386
1546
  fix: isInstalled ? void 0 : "Run the setup_panel tool to install it."
1387
1547
  });
1388
1548
  if (isInstalled && source) {
1389
- const installedHash = sha256(path4.join(installed, "jsx", "bundle.jsx"));
1390
- const sourceHash = sha256(path4.join(source, "jsx", "bundle.jsx"));
1549
+ const installedHash = sha256(path6.join(installed, "jsx", "bundle.jsx"));
1550
+ const sourceHash = sha256(path6.join(source, "jsx", "bundle.jsx"));
1391
1551
  const upToDate = installedHash !== null && installedHash === sourceHash;
1392
1552
  checks.push({
1393
1553
  name: "panelUpToDate",
@@ -1411,6 +1571,16 @@ async function checkSetup() {
1411
1571
  detail: bridge.detail,
1412
1572
  fix: bridge.ok ? void 0 : "If the other checks pass, restart After Effects so the panel reloads."
1413
1573
  });
1574
+ if (bridge.ok && source) {
1575
+ const assessment = assessPanel(bridge.bundleHash, sha256(path6.join(installed, "jsx", "bundle.jsx")));
1576
+ const ok = assessment.state === "current";
1577
+ checks.push({
1578
+ name: "panelRunningCurrent",
1579
+ ok,
1580
+ detail: ok ? "After Effects is running the panel that ships with these tools" : assessment.state === "restart-needed" ? "After Effects is still running the previous panel \u2014 the update needs a restart to take effect" : assessment.state === "unknown" ? "the running panel is too old to report its version" : "After Effects is running a panel older than these tools",
1581
+ fix: ok ? void 0 : assessment.message
1582
+ });
1583
+ }
1414
1584
  if (bridge.ok && !isInstalled) {
1415
1585
  checks.push({
1416
1586
  name: "panelIdentity",
@@ -1443,8 +1613,8 @@ function buildNextSteps(checks, ready) {
1443
1613
  steps.push(identity.fix);
1444
1614
  }
1445
1615
  if (by("afterEffectsRunning")?.ok === false) {
1446
- steps.push("Open After Effects 2026.");
1447
- } else if (needsInstall) {
1616
+ steps.push(needsInstall ? "Open After Effects 2026 \u2014 the panel loads with it." : "Open After Effects 2026.");
1617
+ } else if (needsInstall || by("panelRunningCurrent")?.ok === false) {
1448
1618
  steps.push("Quit and reopen After Effects so it picks up the panel.");
1449
1619
  }
1450
1620
  if (steps.length === 0 && by("bridgeReachable")?.ok === false) {
@@ -1455,8 +1625,8 @@ function buildNextSteps(checks, ready) {
1455
1625
  }
1456
1626
 
1457
1627
  // src/setup/install.ts
1458
- import fs5 from "node:fs";
1459
- import path5 from "node:path";
1628
+ import fs6 from "node:fs";
1629
+ import path7 from "node:path";
1460
1630
  async function installPanel(opts = {}) {
1461
1631
  const actions = [];
1462
1632
  const notes = [];
@@ -1469,13 +1639,13 @@ async function installPanel(opts = {}) {
1469
1639
  if (!source) {
1470
1640
  throw new Error("Could not find the CEP panel assets that ship with this server. Reinstall the package.");
1471
1641
  }
1472
- if (!fs5.existsSync(path5.join(source, "jsx", "bundle.jsx"))) {
1642
+ if (!fs6.existsSync(path7.join(source, "jsx", "bundle.jsx"))) {
1473
1643
  throw new Error(`The panel at ${source} has no jsx/bundle.jsx. In a git checkout, run \`npm run build:jsx\` first.`);
1474
1644
  }
1475
1645
  const target = installedPanelDir();
1476
- const existing = fs5.lstatSync(target, { throwIfNoEntry: false });
1646
+ const existing = fs6.lstatSync(target, { throwIfNoEntry: false });
1477
1647
  if (existing?.isSymbolicLink() && !opts.force) {
1478
- const linkTarget = fs5.readlinkSync(target);
1648
+ const linkTarget = fs6.readlinkSync(target);
1479
1649
  return {
1480
1650
  ok: true,
1481
1651
  panelPath: target,
@@ -1489,17 +1659,17 @@ async function installPanel(opts = {}) {
1489
1659
  };
1490
1660
  }
1491
1661
  if (existing) {
1492
- fs5.rmSync(target, { recursive: true, force: true });
1662
+ fs6.rmSync(target, { recursive: true, force: true });
1493
1663
  actions.push("Removed the previously installed panel.");
1494
1664
  }
1495
- fs5.mkdirSync(path5.dirname(target), { recursive: true });
1665
+ fs6.mkdirSync(path7.dirname(target), { recursive: true });
1496
1666
  copyRecursive(source, target);
1497
1667
  actions.push(`Installed the panel to ${target}.`);
1498
1668
  const ws = wsModuleDir();
1499
1669
  if (ws) {
1500
- const dest = path5.join(target, "node_modules", "ws");
1501
- fs5.mkdirSync(path5.dirname(dest), { recursive: true });
1502
- fs5.rmSync(dest, { recursive: true, force: true });
1670
+ const dest = path7.join(target, "node_modules", "ws");
1671
+ fs6.mkdirSync(path7.dirname(dest), { recursive: true });
1672
+ fs6.rmSync(dest, { recursive: true, force: true });
1503
1673
  copyRecursive(ws, dest);
1504
1674
  actions.push("Copied the `ws` module the panel needs at runtime.");
1505
1675
  } else {
@@ -1533,10 +1703,57 @@ async function installPanel(opts = {}) {
1533
1703
  };
1534
1704
  }
1535
1705
 
1706
+ // src/generated/content.ts
1707
+ var GUIDES = [
1708
+ {
1709
+ name: "ae-setup",
1710
+ description: "Diagnose and repair the connection between the AE MCP tools and After Effects \u2014 panel not installed, AE not running, Adobe debug preference off, bridge not responding. Load when an After Effects tool reports it cannot reach AE, or when the user is setting this up for the first time.",
1711
+ body: "# Getting After Effects connected\n\nThe tools talk to a small panel that runs **inside** After Effects. Three things must be true for that to work: the panel is installed, Adobe is willing to load it, and AE is open.\n\nAssume the person you are helping is a motion designer, not a developer. They should never need to open a terminal \u2014 you have tools for all of this.\n\n## Always start with check_setup\n\n`check_setup` is read-only and safe to call at any time. It returns a `checks` array and a `nextSteps` list already written in plain language.\n\n**Relay `nextSteps` to the user directly.** Do not paraphrase it into jargon, and do not invent steps it did not mention.\n\n## Install before they open After Effects, if you still can\n\nThe panel loads at launch and only at launch. So the order matters, and it is\nthe opposite of what people assume:\n\n- **After Effects is closed** \u2014 install now. When they open it, the panel is\n simply there. No restart, nothing to ask for. This is the good path, and on a\n first-time setup you can usually get it.\n- **After Effects is open** \u2014 install, then they have to quit and reopen it.\n Unavoidable, but worth avoiding: if they have not opened AE yet in this\n conversation, do the install *first* and tell them to open it after.\n\n`check_setup` reports `afterEffectsRunning`, so you always know which case you\nare in before you say anything.\n\n## The repair path\n\n1. **`check_setup`** \u2014 find out what is actually wrong.\n2. **`setup_panel`** \u2014 if the panel is missing or out of date. Tell the user what it will do *before* you call it: it copies the panel into their Adobe extensions folder and switches on the Adobe preference that permits unsigned panels. Both changes are user-level and reversible.\n3. **Get the panel loaded.** If AE was closed, ask them to open it. If it was already open, ask them to quit and reopen it. You cannot do either for them.\n4. **`check_setup`** again to confirm.\n\n## What the individual failures mean\n\n| Check | Meaning when it fails |\n|---|---|\n| `platform` | Not macOS or Windows. After Effects only runs on those two, so there is nothing to fix. |\n| `panelAssetsPresent` | The server package is incomplete \u2014 it needs reinstalling. |\n| `cepDebugMode` | Adobe refuses to load unsigned panels until this preference is on. `setup_panel` sets it. |\n| `panelInstalled` | The panel is not in the Adobe extensions folder yet. `setup_panel` installs it. |\n| `panelUpToDate` | The files on disk are older than this server. Run `setup_panel`. |\n| `panelRunningCurrent` | AE is *running* an older panel than these tools ship. This is the one that predicts whether calls will actually work \u2014 `panelUpToDate` can pass while this fails, for the whole window between installing an update and restarting AE. |\n| `afterEffectsRunning` | AE is closed. If the panel also needs installing, install it now and then ask them to open AE \u2014 that saves a restart. |\n| `bridgeReachable` | Everything is installed but the panel isn't answering \u2014 almost always fixed by restarting AE. |\n\n## The reboot case\n\n`cepDebugMode` is an Adobe preference that, on some macOS builds, only takes effect after a **restart of the Mac** \u2014 not just of After Effects. If `setup_panel` reports `rebootRecommended: true` and restarting AE alone did not fix it, ask the user to reboot once. This is a one-time cost, never needed again.\n\n## When a tool says the panel is out of date\n\nYou may get an error saying the panel is older than these tools, or that it does\nnot recognise an op. That is a version mismatch, not a broken tool, and the\nmessage tells you which of the two fixes applies:\n\n- **\"updated on disk \u2026 still running the previous version\"** \u2014 `setup_panel` has\n already done its part. Only a restart of After Effects will help; running it\n again will not.\n- **anything else** \u2014 run `setup_panel`, then get AE restarted.\n\nEither way, do not retry the failed call until the user confirms AE has\nrestarted. Say it as a version mismatch in plain language, not as a failure:\ntheir tools moved ahead of the panel, and it takes a restart to catch up.\n\n## If it still will not connect\n\nAsk the user to open **Window > Extensions > AE MCP Bridge** inside After Effects. That panel shows its own status and a log, and will say whether it started, which port it took, or what error it hit. Have them read it back to you.\n\nA common cause is a stale install: the panel loaded an older script bundle than the server expects. `check_setup`'s `panelUpToDate` catches that \u2014 the fix is `setup_panel` followed by an AE restart."
1712
+ },
1713
+ {
1714
+ name: "after-effects",
1715
+ description: "How to drive Adobe After Effects well through the AE MCP tools \u2014 orienting in a project, building and animating layers, keyframes and easing, expressions, effects, text and shapes, and the gotchas that silently produce wrong output. Load whenever a task involves After Effects, motion graphics, comps, layers, or keyframes.",
1716
+ body: '# Driving After Effects\n\nYou have direct control of a live After Effects session. The user sees every change immediately, and every tool call is a real undo step in their project. Work like a motion designer at the keyboard, not like a script that fires blind.\n\n## Read the house style first\n\n`get_house_style` returns the style guide for the project that is currently open\n\u2014 palette, type, motion defaults, layout rules \u2014 read from `house-style.md`\nsitting next to the `.aep` file. Call it once at the start of any build task and\nfollow what it says. It costs one cheap call and it is the difference between\nwork that matches everything else the user has made and work that does not.\n\nIf it reports `found: false`, build with sensible defaults and offer once, at the\nend, to capture a style guide from what you just made. Don\'t nag about it.\n\n## Orient before you touch anything\n\nNever guess at project state. Cheap reads exist for exactly this:\n\n| Question | Tool |\n|---|---|\n| What\'s in this project? | `get_project_summary` |\n| What comps exist? | `list_comps` |\n| What\'s in this comp? | `get_comp_tree` |\n| Everything about one layer | `get_layer_full` \u2B50 |\n| Where is a layer, by name/type/effect? | `find_layers` |\n\n`get_layer_full` is the one to reach for. It returns transforms **with their keyframes and expressions**, effects with every parameter, masks, markers, and `sourceRect` (the layer\'s visible bounds) in a single call. Prefer one `get_layer_full` over four narrow queries \u2014 it is faster and it shows you context you did not know to ask for.\n\n## Identify things by ID, never by index\n\nEvery comp and layer has a stable numeric `id`. Layer `index` is a 1-based position that **shifts whenever layers are added, deleted, or reordered**. Store `(compId, layerId)` and pass those. An index captured before a `create_*` call may point at a different layer by the time you use it.\n\n## Read, then write, then verify\n\n1. Read the current state (`get_layer_full`).\n2. Make the change.\n3. Verify by reading back the properties \u2014 not by screenshotting.\n\nProperty values are the ground truth. A screenshot tells you something *looks* wrong; `get_layer_full` tells you *why*.\n\n## Screenshots are a diagnostic, not a feedback loop\n\n`screenshot_frame` and `screenshot_layer` are **one-off checks**. Do not screenshot every frame, do not scrub through time, do not screenshot after every edit.\n\n- Take at most 2\u20133 across an animation \u2014 typically start, middle, end.\n- **Always pass `downsample`** on large comps: `2` for 1080p, `3`\u2013`4` for 4K. A full-resolution 4K frame is large enough to blow out your context in one call.\n- The result reports the dimensions actually returned and warns if the downsample could not be applied \u2014 trust those numbers rather than assuming.\n\nTo check motion, read the keyframe values. That is exact; a picture is not.\n\n## Bulk work goes through run_batch\n\nBuilding 40 layers with 40 separate calls is slow and produces 40 undo steps. `run_batch` runs many ops in one ExtendScript pass as a **single undo step**, which is also what the user expects when they ask to undo "that thing you just built".\n\n- `transactional: true` (the default) rolls back the whole batch on the first error.\n- Over 500 ops it returns a `jobId` and streams progress; call `await_job(jobId)` for the final result.\n\n## Keyframes and easing\n\n`add_keyframe` sets a value at a time. Interpolation is separate:\n\n- `set_interpolation` \u2014 linear / bezier / hold, per keyframe, in and out.\n- `set_temporal_ease` \u2014 influence and speed, the "easy ease" controls.\n- `set_spatial_tangents` \u2014 the shape of a motion path through a position keyframe.\n\n**The array-size trap.** `set_temporal_ease` wants one ease entry *per dimension* for ordinary multi-dimensional properties (Scale, Color), but exactly **one** entry for spatial properties (Position, Anchor Point) regardless of whether the layer is 2D or 3D \u2014 because the ease applies along the motion path, not per axis. If you see `Value array does not have 1 elements`, you fed a spatial property one entry per axis.\n\n## Expressions\n\n`set_expression` takes a `propertyPath` such as `["Transform","Position"]` or `["Effects","Gaussian Blur","Blurriness"]`. Expressions are ExtendScript-flavoured JavaScript evaluated by AE per frame.\n\nExpressions are usually a better answer than dense keyframes for anything procedural \u2014 wiggle, loops, counters, follow-through, time remapping. They stay editable by the user afterwards, where a wall of baked keyframes does not.\n\nUse `get_expression` to read one back and `toggle_expression` to disable without deleting.\n\n## Effects\n\nEffects are added by **matchName**, not display name: `add_effect({matchName: "ADBE Gaussian Blur 2"})`. If you do not know a matchName, call `list_available_effects` and search it \u2014 do not guess. `list_effects` shows what is already on a layer, with every parameter.\n\nSet parameters with `set_effect_param` by parameter name (e.g. `"Blurriness"`).\n\n## Text\n\n`create_text_layer` places **point text anchored at the bounding-box centre**, which is not where you would expect from the visible left edge. The tool defaults to `anchorAlign: "left"` so that `position` lines up with the left edge as a designer would read it. Pass `"center"` or `"right"` when you want those, `"none"` for AE\'s raw behaviour.\n\n`set_text` controls font, size, colour, tracking, leading and justification. To auto-fit a background to text, read `sourceRect` from `get_layer_full` and size the shape from its width and height plus padding.\n\n## Shapes\n\n`add_shape_content` builds one node at a time under `Contents` \u2014 `rect`, `ellipse`, `star`, `path`, `fill`, `stroke`, `trim`, `repeater`, `merge`, `group`. Properties are set with friendly names in the same call (`size`, `position`, `roundness`, `color`, `width`, `lineCap`, \u2026).\n\nThis tool is **all-or-nothing**: if a key cannot be applied, the whole node is removed and you get an error naming the bad key. A success result therefore means everything landed. Don\'t add defensive re-reads for it, but do read the error carefully \u2014 it usually means the property is named differently on that node type, and `get_layer_full` will show you the real name.\n\nFor a custom path, use `{type: "path", vertices: [[x,y], \u2026], closed: true}`. The key is `vertices`, not `points`.\n\n## The escape hatch\n\n`run_jsx` executes arbitrary ExtendScript with `app`, `comp`, `OPS` and the helper functions in scope. Reach for it when a needed operation has no tool \u2014 duplicating a comp, driving the render queue, batch-renaming.\n\nTwo warnings: ExtendScript is **single-threaded**, so a long synchronous loop freezes the user\'s AE UI; and returned objects are flattened, so return a string you have assembled yourself rather than a nested object.\n\n## When something costs you real time\n\nThese tools have rough edges, and the same ones catch every session. Two tools\nexist so that each one is only paid for once.\n\n**`list_known_issues`** \u2014 what earlier sessions hit and how they got past it.\nRead it when a tool fails in a way you do not immediately understand, before you\nstart guessing. The answer is often already there.\n\n**`log_issue`** \u2014 write down what you worked out, the moment you work it out.\n\nLog something when all three are true: it cost real effort, it was the tool\'s\nfault rather than yours, and the next session would hit it too. A schema that\naccepts an argument AE then rejects, an error message that names the wrong\nthing, a property whose real name is nothing like its display name. Not your own\ntypos. Not "I forgot the layer was 3D".\n\nWrite the entry for someone who has not seen the failure: the exact error text,\nthe call that produced it, and a workaround concrete enough to apply directly.\nReuse the existing title when you are extending an entry \u2014 that keeps one good\nrecord instead of five thin ones.\n\n### Then offer to pass it on\n\nIf `log_issue` comes back with `reported: false`, mention it to the user \u2014 but\nfinish the actual work first, and put it at the very end, after you have told\nthem what you built. It is a footnote, not the headline.\n\nSay it the way you would to a colleague who does not write code. What you were\ntrying to do, that it fought back, that you got there anyway, and that you can\nsend it to the people who maintain the tool so the next person does not lose the\nsame time. Something like:\n\n> Done \u2014 the lower third is in. One thing worth mentioning: getting the ease\n> onto that position keyframe took a lot longer than it should have, because the\n> tool kept rejecting a value it had just asked for. I found a way around it and\n> made a note. Want me to send it to the people who maintain this so they can\n> fix it properly?\n\nDo not say "GitHub issue", "file a bug" or "open a ticket" unless they say it\nfirst. If they say yes, use the **report-ae-issue** prompt this server provides\n(`/report-ae-issue` where your client exposes prompts as commands) \u2014 it handles\nthe rest. If they say no, drop it; the note stays and can be offered again\nanother time.\n\nNever claim you have reported something you have not.\n\n## When something is not connected\n\nIf a tool reports it cannot reach After Effects, call `check_setup` and relay its `nextSteps` to the user in plain language. Do not try to diagnose CEP by hand.'
1717
+ },
1718
+ {
1719
+ name: "style-guide",
1720
+ description: "Help a motion designer capture their house style \u2014 palette, type, motion and layout \u2014 into the house-style.md file that sits next to their After Effects project and shapes everything built afterwards. Load when the user asks to create, edit or review their style guide, when they say work does not look like theirs, or when get_house_style reports none exists.",
1721
+ body: '# Capturing a house style\n\nA house style is the difference between an assistant that builds *a* lower third\nand one that builds *their* lower third. It lives in `house-style.md` beside the\n`.aep` file, and `get_house_style` reads it before any build task.\n\nYour job here is to get one written with as little effort from the user as\npossible. They are a motion designer. They know exactly what their work looks\nlike and will struggle to dictate it as a specification \u2014 so do not ask them to.\n\n## Two ways in. Prefer the first.\n\n### 1. Read it off work they already like\n\nThis is far better than any questionnaire, because it produces real numbers\ninstead of adjectives.\n\n1. Ask which comp to learn from \u2014 "point me at something that looks the way you\n want everything to look."\n2. `get_comp` for size and frame rate, then `get_layer_full` on the layers that\n carry the look: the text, the background, the accent shapes.\n3. Pull out the concrete values \u2014 hex colours, font families and sizes, tracking,\n corner radii, stroke widths, the position of things relative to the frame.\n4. Read the keyframes too. `get_keyframes` plus the ease settings tell you the\n timing signature: how long a standard in-animation takes, whether it\n overshoots, whether anything is ever linear.\n5. Show them what you found, in their language, and ask what to change:\n\n > Here\'s what I read off that comp: near-black background `#0B0D12`, white\n > text in Inter Semibold at 64px with slightly tight tracking, one green\n > accent `#3DC46E`. Things scale in over about 0.4s with an overshoot to 108%\n > and easy ease on both ends. Nothing sits perfectly still \u2014 there\'s a slow\n > wiggle on the chip. Does that sound like your style, or was that comp a\n > one-off?\n\n6. Write it with `set_house_style`.\n\n### 2. Ask, when there is nothing to read\n\nOnly if the project is empty or they have no reference. Keep it to four\nquestions, and offer concrete options rather than open ones \u2014 "dark or light\nbackground?" beats "what\'s your palette?". Then build one small example, show it\nwith `screenshot_frame`, and refine from their reaction. Reacting is easier than\nspecifying.\n\n## What makes a guide that actually works\n\n**Numbers, not adjectives.** `#131521 at 92% opacity` is usable. "Dark and clean"\nis not. If a corner radius, a stroke width or a hold duration matters, write the\nnumber. Anything vague will be silently reinterpreted every time it is read.\n\n**Rules, not just values.** The most valuable lines are the prohibitions: "never\nput text directly on footage \u2014 always on a rounded chip", "keep total runtime\nunder 8 seconds", "no linear motion unless something mechanical is moving".\nThose are what stop work drifting.\n\n**Only what you verified.** Do not pad the file with plausible-sounding defaults\nthey never asked for. A short guide that is true beats a complete one that is\nhalf invented. Leave a heading empty rather than filling it with a guess.\n\n## Keep it current\n\nWhen the user corrects the same thing twice \u2014 "no, the accent green, not the\nblue" \u2014 that is a missing rule, not a one-off. Offer to add it:\n\n> I\'ve had to switch that green twice now. Want me to put it in the style guide\n> so it\'s the default from here?\n\nRead the existing guide with `get_house_style` before writing, and preserve what\nis already there. `set_house_style` replaces the whole file, so send back the\nfull document, not just your additions.\n\n## The one thing to warn them about\n\n`house-style.md` is written next to the `.aep`, so **the project has to have been\nsaved at least once** \u2014 an unsaved project has no folder to write into, and\n`get_house_style` will say so. If that happens, ask them to save the project\nfirst, then write the guide.\n\nThe file is plain markdown. Tell them where it is and that they can edit it in\nany text editor without going through you.\n\n## Starting point\n\nWhen writing a guide from scratch, this is the shape to fill in. Drop headings\nyou have nothing real to put under.\n\n```markdown\n# House style\n\n## Palette\n| Role | Colour | Notes |\n|---|---|---|\n| Background | `#0B0D12` | |\n| Primary text | `#FFFFFF` | |\n| Accent | `#3DC46E` | Emphasis and positive values |\n| Negative | `#E03333` | |\n\n## Type\n- Headings: Inter Semibold, 56\u201372px, tracking -10\n- Body: Inter Regular, 28\u201334px\n- Left-aligned unless stated otherwise\n\n## Motion\n- Standard in: scale 0 \u2192 108 \u2192 100, easy ease, ~0.4s\n- Standard out: scale \u2192 0, ~0.3s\n- Easy ease on everything; no linear motion unless mechanical\n- Subtle wiggle on position so nothing sits perfectly still\n\n## Layout\n- 1920\xD71080 at 30fps\n- 120px safe margin from every edge\n- Lower thirds sit bottom-left, above the margin\n\n## Rules\n- Never put text directly on footage \u2014 always on a rounded chip\n- Total runtime under 8 seconds\n```'
1722
+ }
1723
+ ];
1724
+ var PROMPTS = [
1725
+ {
1726
+ name: "create-style-guide",
1727
+ description: "Capture or update the look of this project \u2014 palette, type, motion and layout \u2014 into the style guide that shapes everything built afterwards",
1728
+ argumentHint: "[the comp to learn the style from, if you have one in mind]",
1729
+ body: "# Set up the style guide\n\nThe user wants everything you build to look like *their* work rather than\ngeneric motion graphics. That is what the style guide is for. It is saved as\n`house-style.md` next to their After Effects project and read before every build.\n\n`$ARGUMENTS` may name a comp to learn from.\n\nLoad the `style-guide` topic of `ae_guide` and follow it. In short:\n\n1. `get_house_style` first. If one exists, you are editing, not creating \u2014 read\n it, keep what is there, and send the whole merged document back.\n2. Prefer reading the style off work they already like over asking them to\n describe it. Ask which comp, then `get_comp` and `get_layer_full` on the\n layers that carry the look, and `get_keyframes` for the timing signature.\n3. Show what you found in plain language and let them correct it. Adjectives\n from you, numbers in the file.\n4. Write it with `set_house_style` (`overwrite: true` when replacing), then tell\n them where it is and that they can edit it in any text editor.\n\nTwo things that will stop you: the project must have been **saved** at least\nonce, and `set_house_style` replaces the whole file rather than patching it.\n\nIf they have nothing to learn from, do not run a long interview. Ask four\nquestions with concrete options, build one small example, screenshot it, and\nrefine from their reaction \u2014 reacting is much easier than specifying."
1730
+ },
1731
+ {
1732
+ name: "init-after-effects",
1733
+ description: "Set up After Effects from scratch \u2014 install the panel, create a project folder, and capture a house style",
1734
+ argumentHint: "[folder to set the project up in, if you know it]",
1735
+ body: "# Set up After Effects\n\nThe user has just connected these tools and wants to start working. Take them\nall the way from nothing to a first build. They are a motion designer, not a\ndeveloper \u2014 they should never be asked to open a terminal, edit JSON, or read a\nfile path they did not ask about.\n\n`$ARGUMENTS` is the folder they named, if they named one.\n\nWork through these in order, and **stop at the first one that needs something\nfrom them**. Do not run ahead and report four steps at once.\n\n## 1. Install the panel \u2014 before they open After Effects\n\nCall `check_setup`. It is read-only and safe.\n\n**Do not ask them to open After Effects yet.** The panel only loads when AE\nlaunches, so installing while AE is still closed means it is simply there when\nthey open it \u2014 no restart to ask for. If AE is already running you have to ask\nfor one, which is why this step comes first.\n\n- **Everything green** \u2014 say so in one line and move on.\n- **Anything red** \u2014 explain what `setup_panel` is about to do before calling\n it: it copies a small panel into their Adobe extensions folder and switches on\n the Adobe setting that allows unsigned panels. Both are user-level and\n reversible. Call it, then:\n - if `afterEffectsRunning` was false, ask them to **open** After Effects;\n - if it was true, ask them to **quit and reopen** it.\n\n Then `check_setup` again to confirm.\n\nIf it still fails, load the `ae-setup` topic of `ae_guide` and work through it.\nDo not improvise CEP diagnostics.\n\n## 2. Where does the project live?\n\nCall `init_project`. Pass `dir` when you know it \u2014 from `$ARGUMENTS`, or from\nwhat the user says. If you do not know, **ask before calling**: \"which folder\nshould this project live in? A new empty one is fine.\"\n\nNever invent a path. If the tool reports it could not work out where to write,\nthat is exactly what it means \u2014 ask, then call again with `dir`.\n\nTell them the folder it created and what is in it, in one sentence. Do not paste\nthe file list.\n\n## 3. Now bring up After Effects\n\nBy this point the panel is installed, so this is the moment to have them open\nAfter Effects and load the project they want to work on \u2014 or create one and\n**save** it. Saving matters: the style guide is written next to the .aep, and an\nunsaved project has no folder to put it in.\n\n`get_project_summary` will tell you what is open.\n\n## 4. Offer a style guide\n\nCall `get_house_style`. If one already exists, say what it covers and stop \u2014\nthey are set up.\n\nIf not, offer it in their terms:\n\n> Do you want me to set up a style guide? If you point me at a comp that already\n> looks the way you like, I'll read the colours, fonts and timing off it and save\n> them next to your project. Everything I build afterwards follows it.\n\nIf they say yes, load the `style-guide` topic of `ae_guide` and follow it \u2014 read\na comp they nominate, show them what you found in plain language, and write it\nwith `set_house_style`. If they say no, drop it; it can be offered again later.\n\n## 5. Hand over\n\nClose with one short paragraph: they are set up, and here is the kind of thing\nthey can now ask for. Give one concrete example rather than a list of features:\n\n> You're set. Try something like \"build a lower third that says Chapter One and\n> slides in from the left\" \u2014 I'll read the comp, build it, and you'll see it\n> happen in After Effects."
1736
+ },
1737
+ {
1738
+ name: "report-ae-issue",
1739
+ description: "Send a problem you hit with the After Effects tools to the people who maintain them",
1740
+ argumentHint: "[what went wrong, in your own words]",
1741
+ body: '# Report a problem with the After Effects tools\n\nThe user wants to tell the maintainers about something that did not work. They are\nmost likely a motion designer, not a developer: they may never have seen GitHub,\nand they should not have to. Do the technical part yourself and only ask them\nthings they can actually answer.\n\n`$ARGUMENTS` is what they typed, if anything.\n\n## 1. Find out what to report\n\nCall `list_known_issues` with `status: "unreported"`. It returns entries earlier\nsessions wrote down, plus `repo`, `newIssueUrl`, `serverVersion` and `platform`.\n\n- **Entries exist** \u2014 show them as a short numbered list, one plain sentence each\n ("Text layers ended up in the wrong place when a font was missing"), not the\n raw titles. Ask which to send; offer "all of them" as an option.\n- **No entries, but `$ARGUMENTS` describes something** \u2014 work from that. Ask what\n they were trying to do and what happened instead, then `log_issue` it so it is\n recorded before you send it.\n- **Nothing either way** \u2014 say there is nothing recorded to send, and that you\n will write things down as you hit them from now on. Stop there.\n\n## 2. Draft it\n\nShort. A maintainer should understand the problem in fifteen seconds.\n\n**Title:** one line, concrete. `set_temporal_ease fails on Position with "Value\narray does not have 1 elements"` \u2014 not `Keyframe bug`.\n\n**Body:** four short sections, a couple of sentences each.\n\n```markdown\n**What happens**\n<the failing call and the exact error, or the wrong result>\n\n**Why** (if known)\n<one line \u2014 omit this section entirely if unknown>\n\n**Workaround**\n<what got past it>\n\n**Environment**\nafter-effects-mcp <serverVersion> \xB7 <platform> \xB7 After Effects 2026\n```\n\nInclude the failing call and error text verbatim \u2014 that is the part that makes it\nfixable. Leave out the user\'s own content: comp and layer names from their\nproject, file paths, client names, anything about the video they are making. If a\ndetail like that is load-bearing, replace it with a placeholder.\n\n## 3. Show it and get a yes\n\nShow the finished title and body and ask whether to send it. This posts publicly\nto a repository under their name if `gh` is authenticated, so it needs a real\nanswer, not an assumption. If they want to change the wording, change it.\n\n## 4. Send it\n\nTry `gh` first:\n\n```bash\ngh issue create --repo <repo> --title "<title>" --body "<body>"\n```\n\nIf `gh` is missing or not authenticated, do not try to install or configure it.\nBuild a prefilled link instead \u2014 URL-encode the title and body onto\n`<newIssueUrl>` as `?title=\u2026&body=\u2026` \u2014 and give it to them with one line of\ninstruction: open this, it will already be filled in, press the green button. A\nGitHub account is needed to press it; if they do not have one, say so plainly and\noffer to write the text out for them to send another way.\n\n## 5. Close the loop\n\nOn success, call `mark_issue_reported` with the entry `id` and the URL, so no\nlater session asks them to report the same thing twice. Then tell them where it\nwent, in one sentence, with the link.\n\nIf they decline, leave the entry alone \u2014 it stays unreported and can be offered\nagain another day. Do not mark it.'
1742
+ }
1743
+ ];
1744
+ var GUIDE_NAMES = GUIDES.map((g) => g.name);
1745
+ function getGuide(name) {
1746
+ return GUIDES.find((g) => g.name === name);
1747
+ }
1748
+ function getPrompt(name) {
1749
+ return PROMPTS.find((p) => p.name === name);
1750
+ }
1751
+ var SERVER_INSTRUCTIONS = "You are driving a live After Effects session through this server. The user sees\nevery change as it happens and every call is a real undo step in their project.\n\nSix things that are not obvious from the tool list:\n\n1. Read the house style before you build. `get_house_style` returns the user's\n palette, type and motion defaults for the project that is open. One cheap call.\n2. Orient before you touch anything. `get_layer_full` returns a layer's\n transforms with keyframes and expressions, every effect and parameter, masks,\n markers and visible bounds in a single call \u2014 prefer it over several narrow reads.\n3. Identify by id, never by index. Layer `index` shifts whenever layers are\n added, deleted or reordered. Carry `(compId, layerId)`.\n4. Verify by reading properties back, not by screenshotting. Screenshots are\n one-off diagnostics: 2-3 across an animation, and always pass `downsample`\n (2 for 1080p, 3-4 for 4K) or a single frame can fill your context.\n5. Bulk work goes through `run_batch` \u2014 one ExtendScript pass, one undo step.\n6. When a tool fails in a way you do not understand, call `list_known_issues`\n before guessing; an earlier session may have solved it already. When you solve\n a new one, `log_issue` it.\n\nCall `ae_guide` for the full guidance on any of this \u2014 topics: ae-setup, after-effects, style-guide.\nIf a tool reports it cannot reach After Effects, call `check_setup` and relay\nits `nextSteps` verbatim; do not diagnose CEP by hand.";
1752
+
1536
1753
  // src/issues/journal.ts
1537
- import fs6 from "node:fs";
1538
- import os3 from "node:os";
1539
- import path6 from "node:path";
1754
+ import fs7 from "node:fs";
1755
+ import os4 from "node:os";
1756
+ import path8 from "node:path";
1540
1757
  var REPO = "Engine-Room-Games/after-effects-mcp";
1541
1758
  var NEW_ISSUE_URL = `https://github.com/${REPO}/issues/new`;
1542
1759
  var SECTION_SYMPTOM = "What went wrong";
@@ -1546,25 +1763,25 @@ function journalRoot() {
1546
1763
  const override = process.env.AE_MCP_HOME?.trim();
1547
1764
  if (override && override.length > 0) return { dir: override, scope: "project" };
1548
1765
  const cwd = process.cwd();
1549
- const unusable = cwd === path6.parse(cwd).root || cwd === os3.homedir();
1766
+ const unusable = cwd === path8.parse(cwd).root || cwd === os4.homedir();
1550
1767
  if (!unusable) {
1551
1768
  try {
1552
- fs6.accessSync(cwd, fs6.constants.W_OK);
1553
- return { dir: path6.join(cwd, ".ae-mcp"), scope: "project" };
1769
+ fs7.accessSync(cwd, fs7.constants.W_OK);
1770
+ return { dir: path8.join(cwd, ".ae-mcp"), scope: "project" };
1554
1771
  } catch {
1555
1772
  }
1556
1773
  }
1557
- return { dir: path6.join(os3.homedir(), ".after-effects-mcp"), scope: "home" };
1774
+ return { dir: path8.join(os4.homedir(), ".after-effects-mcp"), scope: "home" };
1558
1775
  }
1559
1776
  function journalDir() {
1560
- return path6.join(journalRoot().dir, "issues");
1777
+ return path8.join(journalRoot().dir, "issues");
1561
1778
  }
1562
1779
  function ensureJournalDir() {
1563
1780
  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");
1781
+ const issues = path8.join(dir, "issues");
1782
+ fs7.mkdirSync(issues, { recursive: true });
1783
+ const ignore = path8.join(dir, ".gitignore");
1784
+ if (!fs7.existsSync(ignore)) fs7.writeFileSync(ignore, "*\n", "utf8");
1568
1785
  return issues;
1569
1786
  }
1570
1787
  function slugify(text) {
@@ -1578,9 +1795,9 @@ function oneLine(text) {
1578
1795
  return text.replace(/\s+/g, " ").trim();
1579
1796
  }
1580
1797
  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}`);
1798
+ const dir = path8.resolve(journalDir());
1799
+ const file = path8.resolve(dir, `${id}.md`);
1800
+ if (path8.dirname(file) !== dir) throw new Error(`Invalid issue id: ${id}`);
1584
1801
  return file;
1585
1802
  }
1586
1803
  function render(entry) {
@@ -1653,7 +1870,7 @@ function parse(text, fallbackId) {
1653
1870
  }
1654
1871
  function readEntry(file) {
1655
1872
  try {
1656
- return parse(fs6.readFileSync(file, "utf8"), path6.basename(file, ".md"));
1873
+ return parse(fs7.readFileSync(file, "utf8"), path8.basename(file, ".md"));
1657
1874
  } catch {
1658
1875
  return null;
1659
1876
  }
@@ -1661,7 +1878,7 @@ function readEntry(file) {
1661
1878
  function logIssue(input) {
1662
1879
  const id = slugify(input.title);
1663
1880
  const file = entryPath(id);
1664
- const existing = fs6.existsSync(file) ? readEntry(file) : null;
1881
+ const existing = fs7.existsSync(file) ? readEntry(file) : null;
1665
1882
  const entry = {
1666
1883
  id,
1667
1884
  title: oneLine(input.title),
@@ -1682,7 +1899,7 @@ function logIssue(input) {
1682
1899
  workaround: input.workaround
1683
1900
  };
1684
1901
  ensureJournalDir();
1685
- fs6.writeFileSync(file, render(entry), "utf8");
1902
+ fs7.writeFileSync(file, render(entry), "utf8");
1686
1903
  return {
1687
1904
  id,
1688
1905
  path: file,
@@ -1697,7 +1914,7 @@ function listIssues(status = "all", tool) {
1697
1914
  const dir = journalDir();
1698
1915
  let entries = [];
1699
1916
  try {
1700
- entries = fs6.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => readEntry(path6.join(dir, f))).filter((e) => e !== null);
1917
+ entries = fs7.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => readEntry(path8.join(dir, f))).filter((e) => e !== null);
1701
1918
  } catch {
1702
1919
  entries = [];
1703
1920
  }
@@ -1722,7 +1939,7 @@ function listIssues(status = "all", tool) {
1722
1939
  }
1723
1940
  function markReported(id, url) {
1724
1941
  const file = entryPath(slugify(id));
1725
- const entry = fs6.existsSync(file) ? readEntry(file) : null;
1942
+ const entry = fs7.existsSync(file) ? readEntry(file) : null;
1726
1943
  if (!entry) {
1727
1944
  const known = listIssues("all").issues.map((e) => e.id);
1728
1945
  throw new Error(
@@ -1731,7 +1948,7 @@ function markReported(id, url) {
1731
1948
  }
1732
1949
  entry.reported = true;
1733
1950
  if (url) entry.issueUrl = oneLine(url);
1734
- fs6.writeFileSync(file, render(entry), "utf8");
1951
+ fs7.writeFileSync(file, render(entry), "utf8");
1735
1952
  return entry;
1736
1953
  }
1737
1954
 
@@ -1746,6 +1963,7 @@ function imageContent(meta, base64) {
1746
1963
  }
1747
1964
 
1748
1965
  // src/server.ts
1966
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1749
1967
  var { OpSchemas: OpSchemas2 } = schemas_exports;
1750
1968
  var VISION_OPS = /* @__PURE__ */ new Set(["screenshot_frame", "screenshot_layer"]);
1751
1969
  var ASYNC_OPS = /* @__PURE__ */ new Set(["run_batch"]);
@@ -1755,22 +1973,32 @@ var SERVER_OPS = /* @__PURE__ */ new Set([
1755
1973
  "cancel_job",
1756
1974
  "check_setup",
1757
1975
  "setup_panel",
1976
+ "init_project",
1977
+ "ae_guide",
1758
1978
  "log_issue",
1759
1979
  "list_known_issues",
1760
1980
  "mark_issue_reported"
1761
1981
  ]);
1982
+ var GUIDE_URI_PREFIX = "ae://guide/";
1762
1983
  var AwaitJobSchema = schemas_exports.AwaitJob;
1763
1984
  var GetJobSchema = schemas_exports.GetJob;
1764
1985
  var CancelJobSchema = schemas_exports.CancelJob;
1765
1986
  function createServer() {
1766
1987
  const server = new Server(
1767
- { name: "after-effects-mcp", version: "0.1.2" },
1768
- { capabilities: { tools: {}, logging: {} } }
1988
+ { name: "after-effects-mcp", version: "0.2.0" },
1989
+ {
1990
+ capabilities: { tools: {}, logging: {}, prompts: {}, resources: {} },
1991
+ // Clients that honour this fold it into the system prompt, which is the
1992
+ // only way non-Claude clients get the cross-cutting guidance at all —
1993
+ // skills and slash commands do not exist outside Claude's own clients.
1994
+ instructions: SERVER_INSTRUCTIONS
1995
+ }
1769
1996
  );
1770
1997
  const bridge = new HttpClient();
1771
1998
  const jobs = new JobManager();
1772
1999
  const ws = new WsClient(bridge.port, jobs);
1773
2000
  ws.start();
2001
+ const panelGate = createPanelGate(bridge);
1774
2002
  bridge.health().then(
1775
2003
  (h) => logger.info(`Bridge healthy on port ${h.port}`),
1776
2004
  (e) => logger.warn(`Bridge not reachable yet: ${e.message}`)
@@ -1790,6 +2018,41 @@ function createServer() {
1790
2018
  });
1791
2019
  return { tools };
1792
2020
  });
2021
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
2022
+ prompts: PROMPTS.map((p) => ({
2023
+ name: p.name,
2024
+ description: p.description,
2025
+ arguments: p.argumentHint ? [{ name: "arguments", description: p.argumentHint, required: false }] : []
2026
+ }))
2027
+ }));
2028
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
2029
+ const prompt = getPrompt(req.params.name);
2030
+ if (!prompt) throw new Error(`Unknown prompt: ${req.params.name}`);
2031
+ const given = req.params.arguments?.arguments ?? "";
2032
+ return {
2033
+ description: prompt.description,
2034
+ messages: [
2035
+ {
2036
+ role: "user",
2037
+ content: { type: "text", text: prompt.body.replaceAll("$ARGUMENTS", given) }
2038
+ }
2039
+ ]
2040
+ };
2041
+ });
2042
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
2043
+ resources: GUIDES.map((g) => ({
2044
+ uri: `${GUIDE_URI_PREFIX}${g.name}`,
2045
+ name: g.name,
2046
+ description: g.description,
2047
+ mimeType: "text/markdown"
2048
+ }))
2049
+ }));
2050
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
2051
+ const uri = req.params.uri;
2052
+ const guide = uri.startsWith(GUIDE_URI_PREFIX) ? getGuide(uri.slice(GUIDE_URI_PREFIX.length)) : void 0;
2053
+ if (!guide) throw new Error(`Unknown resource: ${uri}`);
2054
+ return { contents: [{ uri, mimeType: "text/markdown", text: guide.body }] };
2055
+ });
1793
2056
  server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
1794
2057
  const name = req.params.name;
1795
2058
  const rawArgs = req.params.arguments ?? {};
@@ -1821,8 +2084,28 @@ function createServer() {
1821
2084
  if (name === "setup_panel") {
1822
2085
  const a = schemas_exports.SetupPanel.parse(rawArgs);
1823
2086
  const installed = await installPanel({ enableDebugMode: a.enableDebugMode, force: a.force });
2087
+ panelGate.invalidate();
1824
2088
  return textResult({ ...installed, setup: await checkSetup() });
1825
2089
  }
2090
+ if (name === "init_project") {
2091
+ const a = schemas_exports.InitProject.parse(rawArgs);
2092
+ const client = !a.client || a.client === "auto" ? detectClient(server.getClientVersion()?.name) : a.client;
2093
+ return textResult(
2094
+ scaffold({
2095
+ dir: a.dir,
2096
+ name: a.name,
2097
+ client,
2098
+ withMcpConfig: a.withMcpConfig ?? false,
2099
+ roots: a.dir ? void 0 : await clientRoots(server)
2100
+ })
2101
+ );
2102
+ }
2103
+ if (name === "ae_guide") {
2104
+ const a = schemas_exports.AeGuide.parse(rawArgs);
2105
+ const guide = getGuide(a.topic);
2106
+ if (!guide) return errorResult(`Unknown guide topic: ${a.topic}`);
2107
+ return { content: [{ type: "text", text: guide.body }] };
2108
+ }
1826
2109
  if (name === "log_issue") {
1827
2110
  const a = schemas_exports.LogIssue.parse(rawArgs);
1828
2111
  return textResult(logIssue(a));
@@ -1846,6 +2129,8 @@ function createServer() {
1846
2129
  } catch (e) {
1847
2130
  return errorResult(`Invalid arguments for ${name}: ${e.message}`);
1848
2131
  }
2132
+ const staleness = await panelGate.check();
2133
+ if (staleness) return errorResult(staleness);
1849
2134
  try {
1850
2135
  const result = await bridge.runOp(name, args, progressToken);
1851
2136
  if (ASYNC_OPS.has(name) && isAsyncEnvelope(result)) {
@@ -1884,12 +2169,54 @@ function createServer() {
1884
2169
  return textResult(result);
1885
2170
  } catch (e) {
1886
2171
  if (e instanceof BridgeUnreachableError) return errorResult(e.message);
1887
- if (e instanceof AeError) return errorResult(`AE: ${e.message}${e.line ? ` (line ${e.line})` : ""}`);
2172
+ if (e instanceof AeError) {
2173
+ if (/^Unknown op: /.test(e.message)) {
2174
+ panelGate.invalidate();
2175
+ return errorResult(unknownOpMessage(name));
2176
+ }
2177
+ return errorResult(`AE: ${e.message}${e.line ? ` (line ${e.line})` : ""}`);
2178
+ }
1888
2179
  return errorResult(e.message);
1889
2180
  }
1890
2181
  });
1891
2182
  return server;
1892
2183
  }
2184
+ function createPanelGate(bridge) {
2185
+ const RECHECK_MS = 6e4;
2186
+ let verdict = null;
2187
+ let checkedAt = 0;
2188
+ return {
2189
+ /** The message to return instead of forwarding, or null to proceed. */
2190
+ async check() {
2191
+ if (verdict !== null) return verdict;
2192
+ if (Date.now() - checkedAt < RECHECK_MS) return null;
2193
+ try {
2194
+ const health = await bridge.health();
2195
+ const assessment = assessPanel(health.bundleHash, installedBundleHash(installedPanelDir()));
2196
+ checkedAt = Date.now();
2197
+ verdict = assessment.state === "current" || assessment.state === "unknown" ? null : assessment.message;
2198
+ if (assessment.state === "unknown" && assessment.message) logger.warn(assessment.message);
2199
+ return verdict;
2200
+ } catch {
2201
+ return null;
2202
+ }
2203
+ },
2204
+ invalidate() {
2205
+ verdict = null;
2206
+ checkedAt = 0;
2207
+ }
2208
+ };
2209
+ }
2210
+ async function clientRoots(server) {
2211
+ if (!server.getClientCapabilities()?.roots) return void 0;
2212
+ try {
2213
+ const { roots } = await server.listRoots();
2214
+ return roots.map((r) => r.uri).filter((uri) => uri.startsWith("file://")).map((uri) => fileURLToPath2(uri));
2215
+ } catch (e) {
2216
+ logger.warn(`Client advertised roots but listing them failed: ${e.message}`);
2217
+ return void 0;
2218
+ }
2219
+ }
1893
2220
  function textResult(value) {
1894
2221
  return {
1895
2222
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
@@ -1964,7 +2291,7 @@ ${USAGE}`);
1964
2291
  await server.connect(transport);
1965
2292
  logger.info("MCP server running on stdio");
1966
2293
  }
1967
- var VERSION = "0.1.2";
2294
+ var VERSION = "0.2.0";
1968
2295
  main().catch((e) => {
1969
2296
  logger.error("fatal", e.message);
1970
2297
  process.exit(1);