@engine-room/after-effects-mcp 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -57
- package/bin/server.js +796 -168
- package/package.json +4 -4
- package/panel/CSXS/manifest.xml +2 -2
- package/panel/client/csinterface.js +7 -0
- package/panel/client/main.js +34 -2
- package/panel/jsx/bundle.jsx +118 -1
- package/panel/package.json +2 -2
package/bin/server.js
CHANGED
|
@@ -9,167 +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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
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}
|
|
69
72
|
|
|
70
|
-
After Effects project folder. The tools drive After Effects directly from
|
|
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.
|
|
71
75
|
|
|
72
76
|
## How to work in this folder
|
|
73
77
|
|
|
74
78
|
1. Open After Effects with the project you want to work on.
|
|
75
|
-
2.
|
|
76
|
-
|
|
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".
|
|
77
81
|
3. The current state of the comp is read, the change is made, and you see it.
|
|
78
82
|
|
|
79
83
|
## Style
|
|
80
84
|
|
|
81
|
-
The look of everything built here
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
90
|
+
|
|
91
|
+
## When a tool misbehaves
|
|
92
|
+
|
|
93
|
+
Check \`list_known_issues\` before guessing \u2014 an earlier session may already have
|
|
94
|
+
solved it. Anything newly worked out goes in with \`log_issue\`, and the
|
|
95
|
+
report-ae-issue prompt sends it to the maintainers.
|
|
84
96
|
|
|
85
97
|
## Conventions for this project
|
|
86
98
|
|
|
87
|
-
<!-- Anything specific to this project rather than to your general style:
|
|
88
|
-
|
|
99
|
+
<!-- Anything specific to this project rather than to your general style: naming
|
|
100
|
+
conventions for comps and layers, delivery specs, the client's
|
|
89
101
|
requirements, what lives in which comp. -->
|
|
90
102
|
|
|
91
103
|
- Renders go in \`renders/\`.
|
|
92
104
|
`;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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}`;
|
|
99
118
|
}
|
|
119
|
+
return body;
|
|
100
120
|
}
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
];
|
|
103
197
|
function parseInitArgs(argv) {
|
|
104
|
-
const positional =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
216
|
if (positional.length === 0) return { error: "Missing target directory." };
|
|
109
217
|
if (positional.length > 1) return { error: `Expected one directory, got ${positional.length}.` };
|
|
110
|
-
return { dir: positional[0], withMcp };
|
|
218
|
+
return { dir: positional[0], withMcp, client };
|
|
111
219
|
}
|
|
112
220
|
function runInit(argv) {
|
|
113
221
|
const parsed = parseInitArgs(argv);
|
|
114
222
|
if ("error" in parsed) {
|
|
115
|
-
process.stderr.write(`${parsed.error}
|
|
116
|
-
|
|
117
|
-
Usage: npx @engine-room/after-effects-mcp init <directory> [--no-mcp]
|
|
118
|
-
`);
|
|
119
|
-
return 1;
|
|
120
|
-
}
|
|
121
|
-
const target = path.resolve(parsed.dir);
|
|
122
|
-
const name = path.basename(target);
|
|
123
|
-
const files = [
|
|
124
|
-
["CLAUDE.md", CLAUDE_MD.replace("{{NAME}}", name)],
|
|
125
|
-
[path.join(".claude", "skills", "house-style", "SKILL.md"), HOUSE_STYLE],
|
|
126
|
-
[path.join("renders", ".gitkeep"), ""]
|
|
127
|
-
];
|
|
128
|
-
if (parsed.withMcp) files.push([".mcp.json", MCP_JSON]);
|
|
129
|
-
const existing = files.map(([rel]) => rel).filter((rel) => fs.existsSync(path.join(target, rel)));
|
|
130
|
-
if (existing.length > 0) {
|
|
131
223
|
process.stderr.write(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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)
|
|
136
228
|
`
|
|
137
229
|
);
|
|
138
230
|
return 1;
|
|
139
231
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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`);
|
|
144
253
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
`Next:`,
|
|
154
|
-
` 1. Open the folder in your MCP client: cd ${parsed.dir}`,
|
|
155
|
-
` 2. Open After Effects, then ask it to set up After Effects.`,
|
|
156
|
-
` 3. Fill in .claude/skills/house-style/SKILL.md with your palette, type and timing.`,
|
|
157
|
-
...parsed.withMcp ? [] : [
|
|
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(
|
|
259
|
+
``,
|
|
260
|
+
`${parsed.client} configures servers globally rather than per folder. Add this to`,
|
|
261
|
+
`${result.mcpConfigHint.path}:`,
|
|
158
262
|
``,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
process.stdout.write(out);
|
|
263
|
+
result.mcpConfigHint.json.trimEnd()
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
lines.push(``);
|
|
267
|
+
process.stdout.write(lines.join("\n"));
|
|
165
268
|
return 0;
|
|
166
269
|
}
|
|
270
|
+
function pad(s) {
|
|
271
|
+
return s.padEnd(38);
|
|
272
|
+
}
|
|
167
273
|
|
|
168
274
|
// src/server.ts
|
|
169
275
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
170
276
|
import {
|
|
171
277
|
CallToolRequestSchema,
|
|
172
|
-
|
|
278
|
+
GetPromptRequestSchema,
|
|
279
|
+
ListPromptsRequestSchema,
|
|
280
|
+
ListResourcesRequestSchema,
|
|
281
|
+
ListToolsRequestSchema,
|
|
282
|
+
ReadResourceRequestSchema
|
|
173
283
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
174
284
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
175
285
|
|
|
@@ -232,6 +342,7 @@ __export(schemas_exports, {
|
|
|
232
342
|
AddMask: () => AddMask,
|
|
233
343
|
AddShapeContent: () => AddShapeContent,
|
|
234
344
|
AddTextAnimator: () => AddTextAnimator,
|
|
345
|
+
AeGuide: () => AeGuide,
|
|
235
346
|
AwaitJob: () => AwaitJob,
|
|
236
347
|
CancelJob: () => CancelJob,
|
|
237
348
|
CheckSetup: () => CheckSetup,
|
|
@@ -251,18 +362,24 @@ __export(schemas_exports, {
|
|
|
251
362
|
DeleteLayer: () => DeleteLayer,
|
|
252
363
|
DuplicateLayer: () => DuplicateLayer,
|
|
253
364
|
FindLayers: () => FindLayers,
|
|
365
|
+
GUIDE_TOPICS: () => GUIDE_TOPICS,
|
|
254
366
|
GetComp: () => GetComp,
|
|
255
367
|
GetCompTree: () => GetCompTree,
|
|
256
368
|
GetExpression: () => GetExpression,
|
|
369
|
+
GetHouseStyle: () => GetHouseStyle,
|
|
257
370
|
GetJob: () => GetJob,
|
|
258
371
|
GetKeyframes: () => GetKeyframes,
|
|
259
372
|
GetLayerFull: () => GetLayerFull,
|
|
260
373
|
GetProjectSummary: () => GetProjectSummary,
|
|
374
|
+
InitProject: () => InitProject,
|
|
261
375
|
Interpolation: () => Interpolation,
|
|
262
376
|
ListAvailableEffects: () => ListAvailableEffects,
|
|
263
377
|
ListComps: () => ListComps,
|
|
264
378
|
ListEffects: () => ListEffects,
|
|
379
|
+
ListKnownIssues: () => ListKnownIssues,
|
|
265
380
|
ListLayers: () => ListLayers,
|
|
381
|
+
LogIssue: () => LogIssue,
|
|
382
|
+
MarkIssueReported: () => MarkIssueReported,
|
|
266
383
|
OpSchemas: () => OpSchemas,
|
|
267
384
|
ParentLayer: () => ParentLayer,
|
|
268
385
|
PropertyPath: () => PropertyPath,
|
|
@@ -280,6 +397,7 @@ __export(schemas_exports, {
|
|
|
280
397
|
SetEffectEnabled: () => SetEffectEnabled,
|
|
281
398
|
SetEffectParam: () => SetEffectParam,
|
|
282
399
|
SetExpression: () => SetExpression,
|
|
400
|
+
SetHouseStyle: () => SetHouseStyle,
|
|
283
401
|
SetInterpolation: () => SetInterpolation,
|
|
284
402
|
SetLayer: () => SetLayer,
|
|
285
403
|
SetMask: () => SetMask,
|
|
@@ -664,11 +782,41 @@ var FindLayers = z2.object({
|
|
|
664
782
|
hasEffectMatchName: z2.string().optional()
|
|
665
783
|
});
|
|
666
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();
|
|
667
790
|
var CheckSetup = z2.object({}).strict();
|
|
668
791
|
var SetupPanel = z2.object({
|
|
669
792
|
enableDebugMode: z2.boolean().default(true).optional().describe("Also enable Adobe's PlayerDebugMode preference, which AE requires to load this unsigned panel. Default true."),
|
|
670
793
|
force: z2.boolean().default(false).optional().describe("Replace an existing symlinked (development) install with a copy. Default false.")
|
|
671
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();
|
|
805
|
+
var LogIssue = z2.object({
|
|
806
|
+
title: z2.string().min(3).describe("One line naming the problem, specific enough to recognise again. Becomes the entry's id."),
|
|
807
|
+
symptom: z2.string().min(3).describe("What went wrong, including the exact error text and the call that produced it."),
|
|
808
|
+
workaround: z2.string().min(3).describe("What actually worked \u2014 concrete enough for the next session to apply without rediscovering it."),
|
|
809
|
+
cause: z2.string().optional().describe("Why it happens, if you worked it out."),
|
|
810
|
+
tools: z2.array(z2.string()).optional().describe("Tool names involved, e.g. ['set_temporal_ease'].")
|
|
811
|
+
}).strict();
|
|
812
|
+
var ListKnownIssues = z2.object({
|
|
813
|
+
status: z2.enum(["all", "unreported", "reported"]).default("all").optional(),
|
|
814
|
+
tool: z2.string().optional().describe("Only entries about this tool, e.g. 'set_temporal_ease'. Omit for everything.")
|
|
815
|
+
}).strict();
|
|
816
|
+
var MarkIssueReported = z2.object({
|
|
817
|
+
id: z2.string().describe("The entry id returned by log_issue or list_known_issues."),
|
|
818
|
+
url: z2.string().optional().describe("Link to the issue that was opened.")
|
|
819
|
+
}).strict();
|
|
672
820
|
var AwaitJob = z2.object({ jobId: z2.string(), timeoutMs: z2.number().int().positive().default(6e5).optional() });
|
|
673
821
|
var GetJob = z2.object({ jobId: z2.string() });
|
|
674
822
|
var CancelJob = z2.object({ jobId: z2.string() });
|
|
@@ -742,13 +890,23 @@ var OpSchemas = {
|
|
|
742
890
|
find_layers: FindLayers,
|
|
743
891
|
// raw
|
|
744
892
|
run_jsx: RunJsx,
|
|
893
|
+
// house style
|
|
894
|
+
get_house_style: GetHouseStyle,
|
|
895
|
+
set_house_style: SetHouseStyle,
|
|
745
896
|
// jobs
|
|
746
897
|
await_job: AwaitJob,
|
|
747
898
|
get_job: GetJob,
|
|
748
899
|
cancel_job: CancelJob,
|
|
749
900
|
// setup
|
|
750
901
|
check_setup: CheckSetup,
|
|
751
|
-
setup_panel: SetupPanel
|
|
902
|
+
setup_panel: SetupPanel,
|
|
903
|
+
init_project: InitProject,
|
|
904
|
+
// guidance
|
|
905
|
+
ae_guide: AeGuide,
|
|
906
|
+
// issue journal
|
|
907
|
+
log_issue: LogIssue,
|
|
908
|
+
list_known_issues: ListKnownIssues,
|
|
909
|
+
mark_issue_reported: MarkIssueReported
|
|
752
910
|
};
|
|
753
911
|
|
|
754
912
|
// src/util/errors.ts
|
|
@@ -793,8 +951,8 @@ var logger = {
|
|
|
793
951
|
|
|
794
952
|
// src/bridge/discovery.ts
|
|
795
953
|
import fs2 from "node:fs";
|
|
796
|
-
import
|
|
797
|
-
import
|
|
954
|
+
import os2 from "node:os";
|
|
955
|
+
import path3 from "node:path";
|
|
798
956
|
var DEFAULT_PORT = 7777;
|
|
799
957
|
function discoverPort() {
|
|
800
958
|
const envPort = process.env.AE_MCP_PORT;
|
|
@@ -803,7 +961,7 @@ function discoverPort() {
|
|
|
803
961
|
if (Number.isFinite(n)) return n;
|
|
804
962
|
}
|
|
805
963
|
try {
|
|
806
|
-
const f =
|
|
964
|
+
const f = path3.join(os2.homedir(), ".engineroom-ae-mcp", "port");
|
|
807
965
|
if (fs2.existsSync(f)) {
|
|
808
966
|
const txt = fs2.readFileSync(f, "utf8").trim();
|
|
809
967
|
const n = parseInt(txt, 10);
|
|
@@ -822,6 +980,8 @@ var HttpClient = class {
|
|
|
822
980
|
this.port = port ?? discoverPort();
|
|
823
981
|
this.base = `http://127.0.0.1:${this.port}`;
|
|
824
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.
|
|
825
985
|
async health() {
|
|
826
986
|
try {
|
|
827
987
|
const r = await fetch(`${this.base}/health`, { signal: AbortSignal.timeout(2e3) });
|
|
@@ -1100,24 +1260,34 @@ var descriptions = {
|
|
|
1100
1260
|
find_layers: "Search across one or all comps for layers matching name/type/effect filters.",
|
|
1101
1261
|
// ---------- raw ----------
|
|
1102
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.",
|
|
1103
1268
|
// ---------- jobs ----------
|
|
1104
1269
|
await_job: "Block until job is done. Default 10min timeout. Returns the same payload the tool would have.",
|
|
1105
1270
|
get_job: "Non-blocking job status: progress/total/state/error.",
|
|
1106
1271
|
cancel_job: "Set cancel flag; chunked loop stops at next boundary.",
|
|
1107
1272
|
// ---------- setup ----------
|
|
1108
|
-
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
|
|
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.",
|
|
1276
|
+
// ---------- issue journal ----------
|
|
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.",
|
|
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.",
|
|
1279
|
+
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
1280
|
};
|
|
1111
1281
|
|
|
1112
1282
|
// src/setup/check.ts
|
|
1113
|
-
import
|
|
1114
|
-
import
|
|
1115
|
-
import
|
|
1283
|
+
import crypto2 from "node:crypto";
|
|
1284
|
+
import fs5 from "node:fs";
|
|
1285
|
+
import path6 from "node:path";
|
|
1116
1286
|
|
|
1117
1287
|
// src/setup/paths.ts
|
|
1118
1288
|
import fs3 from "node:fs";
|
|
1119
|
-
import
|
|
1120
|
-
import
|
|
1289
|
+
import os3 from "node:os";
|
|
1290
|
+
import path4 from "node:path";
|
|
1121
1291
|
import { createRequire } from "node:module";
|
|
1122
1292
|
import { fileURLToPath } from "node:url";
|
|
1123
1293
|
var BUNDLE_ID = "games.engine-room.ae-mcp";
|
|
@@ -1125,48 +1295,64 @@ function isSupportedPlatform() {
|
|
|
1125
1295
|
return process.platform === "darwin" || process.platform === "win32";
|
|
1126
1296
|
}
|
|
1127
1297
|
function packageRoot() {
|
|
1128
|
-
let dir =
|
|
1298
|
+
let dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1129
1299
|
for (let i = 0; i < 8; i++) {
|
|
1130
|
-
if (fs3.existsSync(
|
|
1131
|
-
const parent =
|
|
1300
|
+
if (fs3.existsSync(path4.join(dir, "package.json"))) return dir;
|
|
1301
|
+
const parent = path4.dirname(dir);
|
|
1132
1302
|
if (parent === dir) break;
|
|
1133
1303
|
dir = parent;
|
|
1134
1304
|
}
|
|
1135
|
-
return
|
|
1305
|
+
return path4.dirname(fileURLToPath(import.meta.url));
|
|
1306
|
+
}
|
|
1307
|
+
function executableDir() {
|
|
1308
|
+
return path4.dirname(process.execPath);
|
|
1309
|
+
}
|
|
1310
|
+
function packageVersion() {
|
|
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
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return "unknown";
|
|
1136
1319
|
}
|
|
1137
1320
|
function panelSourceDir() {
|
|
1138
1321
|
const candidates = [
|
|
1139
1322
|
// The live workspace copy comes first so a git checkout always installs
|
|
1140
1323
|
// what the developer is editing, never a stale vendored copy left behind by
|
|
1141
1324
|
// a previous `npm pack`. Only the second path exists in the tarball.
|
|
1142
|
-
|
|
1143
|
-
|
|
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")
|
|
1144
1329
|
];
|
|
1145
1330
|
for (const dir of candidates) {
|
|
1146
|
-
if (fs3.existsSync(
|
|
1331
|
+
if (fs3.existsSync(path4.join(dir, "CSXS", "manifest.xml"))) return dir;
|
|
1147
1332
|
}
|
|
1148
1333
|
return null;
|
|
1149
1334
|
}
|
|
1150
1335
|
function cepExtensionsDir() {
|
|
1151
1336
|
if (process.platform === "win32") {
|
|
1152
|
-
const appData = process.env.APPDATA ??
|
|
1153
|
-
return
|
|
1337
|
+
const appData = process.env.APPDATA ?? path4.join(os3.homedir(), "AppData", "Roaming");
|
|
1338
|
+
return path4.join(appData, "Adobe", "CEP", "extensions");
|
|
1154
1339
|
}
|
|
1155
|
-
return
|
|
1340
|
+
return path4.join(os3.homedir(), "Library", "Application Support", "Adobe", "CEP", "extensions");
|
|
1156
1341
|
}
|
|
1157
1342
|
function installedPanelDir() {
|
|
1158
|
-
return
|
|
1343
|
+
return path4.join(cepExtensionsDir(), BUNDLE_ID);
|
|
1159
1344
|
}
|
|
1160
1345
|
function wsModuleDir() {
|
|
1161
1346
|
try {
|
|
1162
1347
|
const require2 = createRequire(import.meta.url);
|
|
1163
1348
|
const entry = require2.resolve("ws");
|
|
1164
|
-
const marker = `${
|
|
1349
|
+
const marker = `${path4.sep}node_modules${path4.sep}ws${path4.sep}`;
|
|
1165
1350
|
const idx = entry.lastIndexOf(marker);
|
|
1166
1351
|
if (idx >= 0) return entry.slice(0, idx + marker.length - 1);
|
|
1167
|
-
return
|
|
1352
|
+
return path4.dirname(entry);
|
|
1168
1353
|
} catch {
|
|
1169
|
-
|
|
1354
|
+
const beside = path4.join(executableDir(), "node_modules", "ws");
|
|
1355
|
+
return fs3.existsSync(beside) ? beside : null;
|
|
1170
1356
|
}
|
|
1171
1357
|
}
|
|
1172
1358
|
function copyRecursive(src, dst) {
|
|
@@ -1174,7 +1360,7 @@ function copyRecursive(src, dst) {
|
|
|
1174
1360
|
if (stat.isDirectory()) {
|
|
1175
1361
|
fs3.mkdirSync(dst, { recursive: true });
|
|
1176
1362
|
for (const entry of fs3.readdirSync(src)) {
|
|
1177
|
-
copyRecursive(
|
|
1363
|
+
copyRecursive(path4.join(src, entry), path4.join(dst, entry));
|
|
1178
1364
|
}
|
|
1179
1365
|
} else if (stat.isSymbolicLink()) {
|
|
1180
1366
|
fs3.symlinkSync(fs3.readlinkSync(src), dst);
|
|
@@ -1183,6 +1369,56 @@ function copyRecursive(src, dst) {
|
|
|
1183
1369
|
}
|
|
1184
1370
|
}
|
|
1185
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
|
+
|
|
1186
1422
|
// src/setup/platform.ts
|
|
1187
1423
|
import { execFile } from "node:child_process";
|
|
1188
1424
|
import { promisify } from "node:util";
|
|
@@ -1263,7 +1499,7 @@ function debugModeLocation() {
|
|
|
1263
1499
|
// src/setup/check.ts
|
|
1264
1500
|
function sha256(file) {
|
|
1265
1501
|
try {
|
|
1266
|
-
return
|
|
1502
|
+
return crypto2.createHash("sha256").update(fs5.readFileSync(file)).digest("hex");
|
|
1267
1503
|
} catch {
|
|
1268
1504
|
return null;
|
|
1269
1505
|
}
|
|
@@ -1272,7 +1508,8 @@ async function bridgeReachable(port) {
|
|
|
1272
1508
|
try {
|
|
1273
1509
|
const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2e3) });
|
|
1274
1510
|
if (!res.ok) return { ok: false, detail: `port ${port} returned HTTP ${res.status}` };
|
|
1275
|
-
|
|
1511
|
+
const body = await res.json().catch(() => ({}));
|
|
1512
|
+
return { ok: true, detail: `responding on port ${port}`, bundleHash: body.bundleHash };
|
|
1276
1513
|
} catch (e) {
|
|
1277
1514
|
return { ok: false, detail: `no response on port ${port} (${e.message})` };
|
|
1278
1515
|
}
|
|
@@ -1301,7 +1538,7 @@ async function checkSetup() {
|
|
|
1301
1538
|
fix: debugMode.on ? void 0 : `Run the setup_panel tool. After Effects only loads unsigned panels when ${debugModeLocation()} is set.`
|
|
1302
1539
|
});
|
|
1303
1540
|
const installed = installedPanelDir();
|
|
1304
|
-
const isInstalled =
|
|
1541
|
+
const isInstalled = fs5.existsSync(path6.join(installed, "CSXS", "manifest.xml"));
|
|
1305
1542
|
checks.push({
|
|
1306
1543
|
name: "panelInstalled",
|
|
1307
1544
|
ok: isInstalled,
|
|
@@ -1309,8 +1546,8 @@ async function checkSetup() {
|
|
|
1309
1546
|
fix: isInstalled ? void 0 : "Run the setup_panel tool to install it."
|
|
1310
1547
|
});
|
|
1311
1548
|
if (isInstalled && source) {
|
|
1312
|
-
const installedHash = sha256(
|
|
1313
|
-
const sourceHash = sha256(
|
|
1549
|
+
const installedHash = sha256(path6.join(installed, "jsx", "bundle.jsx"));
|
|
1550
|
+
const sourceHash = sha256(path6.join(source, "jsx", "bundle.jsx"));
|
|
1314
1551
|
const upToDate = installedHash !== null && installedHash === sourceHash;
|
|
1315
1552
|
checks.push({
|
|
1316
1553
|
name: "panelUpToDate",
|
|
@@ -1334,6 +1571,16 @@ async function checkSetup() {
|
|
|
1334
1571
|
detail: bridge.detail,
|
|
1335
1572
|
fix: bridge.ok ? void 0 : "If the other checks pass, restart After Effects so the panel reloads."
|
|
1336
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
|
+
}
|
|
1337
1584
|
if (bridge.ok && !isInstalled) {
|
|
1338
1585
|
checks.push({
|
|
1339
1586
|
name: "panelIdentity",
|
|
@@ -1366,8 +1613,8 @@ function buildNextSteps(checks, ready) {
|
|
|
1366
1613
|
steps.push(identity.fix);
|
|
1367
1614
|
}
|
|
1368
1615
|
if (by("afterEffectsRunning")?.ok === false) {
|
|
1369
|
-
steps.push("Open After Effects 2026.");
|
|
1370
|
-
} 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) {
|
|
1371
1618
|
steps.push("Quit and reopen After Effects so it picks up the panel.");
|
|
1372
1619
|
}
|
|
1373
1620
|
if (steps.length === 0 && by("bridgeReachable")?.ok === false) {
|
|
@@ -1378,8 +1625,8 @@ function buildNextSteps(checks, ready) {
|
|
|
1378
1625
|
}
|
|
1379
1626
|
|
|
1380
1627
|
// src/setup/install.ts
|
|
1381
|
-
import
|
|
1382
|
-
import
|
|
1628
|
+
import fs6 from "node:fs";
|
|
1629
|
+
import path7 from "node:path";
|
|
1383
1630
|
async function installPanel(opts = {}) {
|
|
1384
1631
|
const actions = [];
|
|
1385
1632
|
const notes = [];
|
|
@@ -1392,13 +1639,13 @@ async function installPanel(opts = {}) {
|
|
|
1392
1639
|
if (!source) {
|
|
1393
1640
|
throw new Error("Could not find the CEP panel assets that ship with this server. Reinstall the package.");
|
|
1394
1641
|
}
|
|
1395
|
-
if (!
|
|
1642
|
+
if (!fs6.existsSync(path7.join(source, "jsx", "bundle.jsx"))) {
|
|
1396
1643
|
throw new Error(`The panel at ${source} has no jsx/bundle.jsx. In a git checkout, run \`npm run build:jsx\` first.`);
|
|
1397
1644
|
}
|
|
1398
1645
|
const target = installedPanelDir();
|
|
1399
|
-
const existing =
|
|
1646
|
+
const existing = fs6.lstatSync(target, { throwIfNoEntry: false });
|
|
1400
1647
|
if (existing?.isSymbolicLink() && !opts.force) {
|
|
1401
|
-
const linkTarget =
|
|
1648
|
+
const linkTarget = fs6.readlinkSync(target);
|
|
1402
1649
|
return {
|
|
1403
1650
|
ok: true,
|
|
1404
1651
|
panelPath: target,
|
|
@@ -1412,17 +1659,17 @@ async function installPanel(opts = {}) {
|
|
|
1412
1659
|
};
|
|
1413
1660
|
}
|
|
1414
1661
|
if (existing) {
|
|
1415
|
-
|
|
1662
|
+
fs6.rmSync(target, { recursive: true, force: true });
|
|
1416
1663
|
actions.push("Removed the previously installed panel.");
|
|
1417
1664
|
}
|
|
1418
|
-
|
|
1665
|
+
fs6.mkdirSync(path7.dirname(target), { recursive: true });
|
|
1419
1666
|
copyRecursive(source, target);
|
|
1420
1667
|
actions.push(`Installed the panel to ${target}.`);
|
|
1421
1668
|
const ws = wsModuleDir();
|
|
1422
1669
|
if (ws) {
|
|
1423
|
-
const dest =
|
|
1424
|
-
|
|
1425
|
-
|
|
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 });
|
|
1426
1673
|
copyRecursive(ws, dest);
|
|
1427
1674
|
actions.push("Copied the `ws` module the panel needs at runtime.");
|
|
1428
1675
|
} else {
|
|
@@ -1456,6 +1703,255 @@ async function installPanel(opts = {}) {
|
|
|
1456
1703
|
};
|
|
1457
1704
|
}
|
|
1458
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
|
+
|
|
1753
|
+
// src/issues/journal.ts
|
|
1754
|
+
import fs7 from "node:fs";
|
|
1755
|
+
import os4 from "node:os";
|
|
1756
|
+
import path8 from "node:path";
|
|
1757
|
+
var REPO = "Engine-Room-Games/after-effects-mcp";
|
|
1758
|
+
var NEW_ISSUE_URL = `https://github.com/${REPO}/issues/new`;
|
|
1759
|
+
var SECTION_SYMPTOM = "What went wrong";
|
|
1760
|
+
var SECTION_CAUSE = "Why";
|
|
1761
|
+
var SECTION_WORKAROUND = "What worked";
|
|
1762
|
+
function journalRoot() {
|
|
1763
|
+
const override = process.env.AE_MCP_HOME?.trim();
|
|
1764
|
+
if (override && override.length > 0) return { dir: override, scope: "project" };
|
|
1765
|
+
const cwd = process.cwd();
|
|
1766
|
+
const unusable = cwd === path8.parse(cwd).root || cwd === os4.homedir();
|
|
1767
|
+
if (!unusable) {
|
|
1768
|
+
try {
|
|
1769
|
+
fs7.accessSync(cwd, fs7.constants.W_OK);
|
|
1770
|
+
return { dir: path8.join(cwd, ".ae-mcp"), scope: "project" };
|
|
1771
|
+
} catch {
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
return { dir: path8.join(os4.homedir(), ".after-effects-mcp"), scope: "home" };
|
|
1775
|
+
}
|
|
1776
|
+
function journalDir() {
|
|
1777
|
+
return path8.join(journalRoot().dir, "issues");
|
|
1778
|
+
}
|
|
1779
|
+
function ensureJournalDir() {
|
|
1780
|
+
const { dir } = journalRoot();
|
|
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");
|
|
1785
|
+
return issues;
|
|
1786
|
+
}
|
|
1787
|
+
function slugify(text) {
|
|
1788
|
+
const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
|
|
1789
|
+
return slug.length > 0 ? slug : `issue-${Date.now()}`;
|
|
1790
|
+
}
|
|
1791
|
+
function today() {
|
|
1792
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1793
|
+
}
|
|
1794
|
+
function oneLine(text) {
|
|
1795
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1796
|
+
}
|
|
1797
|
+
function entryPath(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}`);
|
|
1801
|
+
return file;
|
|
1802
|
+
}
|
|
1803
|
+
function render(entry) {
|
|
1804
|
+
const lines = [
|
|
1805
|
+
"---",
|
|
1806
|
+
`id: ${entry.id}`,
|
|
1807
|
+
`title: ${oneLine(entry.title)}`,
|
|
1808
|
+
`tools: ${entry.tools.join(", ")}`,
|
|
1809
|
+
`firstSeen: ${entry.firstSeen}`,
|
|
1810
|
+
`lastSeen: ${entry.lastSeen}`,
|
|
1811
|
+
`occurrences: ${entry.occurrences}`,
|
|
1812
|
+
`reported: ${entry.reported}`,
|
|
1813
|
+
`issueUrl: ${entry.issueUrl ?? ""}`,
|
|
1814
|
+
"---",
|
|
1815
|
+
"",
|
|
1816
|
+
`## ${SECTION_SYMPTOM}`,
|
|
1817
|
+
"",
|
|
1818
|
+
entry.symptom.trim(),
|
|
1819
|
+
""
|
|
1820
|
+
];
|
|
1821
|
+
if (entry.cause && entry.cause.trim().length > 0) {
|
|
1822
|
+
lines.push(`## ${SECTION_CAUSE}`, "", entry.cause.trim(), "");
|
|
1823
|
+
}
|
|
1824
|
+
lines.push(`## ${SECTION_WORKAROUND}`, "", entry.workaround.trim(), "");
|
|
1825
|
+
return lines.join("\n");
|
|
1826
|
+
}
|
|
1827
|
+
function readSections(body) {
|
|
1828
|
+
const marks = [];
|
|
1829
|
+
for (const heading of [SECTION_SYMPTOM, SECTION_CAUSE, SECTION_WORKAROUND]) {
|
|
1830
|
+
const m = new RegExp(`^##[ \\t]+${heading}[ \\t]*$`, "im").exec(body);
|
|
1831
|
+
if (m) marks.push({ key: heading.toLowerCase(), from: m.index, to: m.index + m[0].length });
|
|
1832
|
+
}
|
|
1833
|
+
marks.sort((a, b) => a.from - b.from);
|
|
1834
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1835
|
+
marks.forEach((mark, i) => {
|
|
1836
|
+
const end = i + 1 < marks.length ? marks[i + 1].from : body.length;
|
|
1837
|
+
sections.set(mark.key, body.slice(mark.to, end).trim());
|
|
1838
|
+
});
|
|
1839
|
+
return sections;
|
|
1840
|
+
}
|
|
1841
|
+
function parse(text, fallbackId) {
|
|
1842
|
+
const meta = {};
|
|
1843
|
+
let body = text;
|
|
1844
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
|
1845
|
+
if (fm) {
|
|
1846
|
+
for (const line of fm[1].split(/\r?\n/)) {
|
|
1847
|
+
const sep = line.indexOf(":");
|
|
1848
|
+
if (sep <= 0) continue;
|
|
1849
|
+
meta[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
|
|
1850
|
+
}
|
|
1851
|
+
body = text.slice(fm[0].length);
|
|
1852
|
+
}
|
|
1853
|
+
const sections = readSections(body);
|
|
1854
|
+
const occurrences = Number.parseInt(meta.occurrences ?? "1", 10);
|
|
1855
|
+
return {
|
|
1856
|
+
id: meta.id || fallbackId,
|
|
1857
|
+
title: meta.title || fallbackId.replace(/-/g, " "),
|
|
1858
|
+
tools: (meta.tools ?? "").split(",").map((t) => t.trim()).filter((t) => t.length > 0),
|
|
1859
|
+
firstSeen: meta.firstSeen || "",
|
|
1860
|
+
lastSeen: meta.lastSeen || meta.firstSeen || "",
|
|
1861
|
+
occurrences: Number.isFinite(occurrences) && occurrences > 0 ? occurrences : 1,
|
|
1862
|
+
reported: meta.reported === "true",
|
|
1863
|
+
issueUrl: meta.issueUrl && meta.issueUrl.length > 0 ? meta.issueUrl : void 0,
|
|
1864
|
+
// A hand-edited file with no recognised headings still has its text kept,
|
|
1865
|
+
// rather than being silently reduced to an empty entry.
|
|
1866
|
+
symptom: sections.get(SECTION_SYMPTOM.toLowerCase()) ?? (sections.size === 0 ? body.trim() : ""),
|
|
1867
|
+
cause: sections.get(SECTION_CAUSE.toLowerCase()) || void 0,
|
|
1868
|
+
workaround: sections.get(SECTION_WORKAROUND.toLowerCase()) ?? ""
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function readEntry(file) {
|
|
1872
|
+
try {
|
|
1873
|
+
return parse(fs7.readFileSync(file, "utf8"), path8.basename(file, ".md"));
|
|
1874
|
+
} catch {
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
function logIssue(input) {
|
|
1879
|
+
const id = slugify(input.title);
|
|
1880
|
+
const file = entryPath(id);
|
|
1881
|
+
const existing = fs7.existsSync(file) ? readEntry(file) : null;
|
|
1882
|
+
const entry = {
|
|
1883
|
+
id,
|
|
1884
|
+
title: oneLine(input.title),
|
|
1885
|
+
tools: input.tools ?? existing?.tools ?? [],
|
|
1886
|
+
firstSeen: existing?.firstSeen || today(),
|
|
1887
|
+
lastSeen: today(),
|
|
1888
|
+
// Repeats are worth counting: an entry seen five times is the one most
|
|
1889
|
+
// worth reporting, and the count is the only evidence of that.
|
|
1890
|
+
occurrences: (existing?.occurrences ?? 0) + 1,
|
|
1891
|
+
// Reporting state belongs to the entry, not to this sighting — a fresh
|
|
1892
|
+
// description of a known problem must not un-report it.
|
|
1893
|
+
reported: existing?.reported ?? false,
|
|
1894
|
+
issueUrl: existing?.issueUrl,
|
|
1895
|
+
symptom: input.symptom,
|
|
1896
|
+
// A cause worked out once is not lost because a later sighting was logged
|
|
1897
|
+
// without one.
|
|
1898
|
+
cause: input.cause ?? existing?.cause,
|
|
1899
|
+
workaround: input.workaround
|
|
1900
|
+
};
|
|
1901
|
+
ensureJournalDir();
|
|
1902
|
+
fs7.writeFileSync(file, render(entry), "utf8");
|
|
1903
|
+
return {
|
|
1904
|
+
id,
|
|
1905
|
+
path: file,
|
|
1906
|
+
occurrences: entry.occurrences,
|
|
1907
|
+
previouslyLogged: existing !== null,
|
|
1908
|
+
reported: entry.reported,
|
|
1909
|
+
issueUrl: entry.issueUrl
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
function listIssues(status = "all", tool) {
|
|
1913
|
+
const { scope } = journalRoot();
|
|
1914
|
+
const dir = journalDir();
|
|
1915
|
+
let entries = [];
|
|
1916
|
+
try {
|
|
1917
|
+
entries = fs7.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => readEntry(path8.join(dir, f))).filter((e) => e !== null);
|
|
1918
|
+
} catch {
|
|
1919
|
+
entries = [];
|
|
1920
|
+
}
|
|
1921
|
+
const wanted = tool?.trim().toLowerCase();
|
|
1922
|
+
const filtered = entries.filter((e) => {
|
|
1923
|
+
const byStatus = status === "all" ? true : status === "reported" ? e.reported : !e.reported;
|
|
1924
|
+
if (!byStatus) return false;
|
|
1925
|
+
if (!wanted) return true;
|
|
1926
|
+
return e.tools.some((t) => t.toLowerCase() === wanted) || e.title.toLowerCase().includes(wanted);
|
|
1927
|
+
});
|
|
1928
|
+
filtered.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen) || b.occurrences - a.occurrences);
|
|
1929
|
+
return {
|
|
1930
|
+
dir,
|
|
1931
|
+
scope,
|
|
1932
|
+
repo: REPO,
|
|
1933
|
+
newIssueUrl: NEW_ISSUE_URL,
|
|
1934
|
+
serverVersion: packageVersion(),
|
|
1935
|
+
platform: process.platform,
|
|
1936
|
+
count: filtered.length,
|
|
1937
|
+
issues: filtered
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
function markReported(id, url) {
|
|
1941
|
+
const file = entryPath(slugify(id));
|
|
1942
|
+
const entry = fs7.existsSync(file) ? readEntry(file) : null;
|
|
1943
|
+
if (!entry) {
|
|
1944
|
+
const known = listIssues("all").issues.map((e) => e.id);
|
|
1945
|
+
throw new Error(
|
|
1946
|
+
`No journal entry with id "${id}".` + (known.length > 0 ? ` Known ids: ${known.join(", ")}` : "")
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
entry.reported = true;
|
|
1950
|
+
if (url) entry.issueUrl = oneLine(url);
|
|
1951
|
+
fs7.writeFileSync(file, render(entry), "utf8");
|
|
1952
|
+
return entry;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1459
1955
|
// src/util/pngImage.ts
|
|
1460
1956
|
function imageContent(meta, base64) {
|
|
1461
1957
|
return {
|
|
@@ -1467,22 +1963,42 @@ function imageContent(meta, base64) {
|
|
|
1467
1963
|
}
|
|
1468
1964
|
|
|
1469
1965
|
// src/server.ts
|
|
1966
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1470
1967
|
var { OpSchemas: OpSchemas2 } = schemas_exports;
|
|
1471
1968
|
var VISION_OPS = /* @__PURE__ */ new Set(["screenshot_frame", "screenshot_layer"]);
|
|
1472
1969
|
var ASYNC_OPS = /* @__PURE__ */ new Set(["run_batch"]);
|
|
1473
|
-
var SERVER_OPS = /* @__PURE__ */ new Set([
|
|
1970
|
+
var SERVER_OPS = /* @__PURE__ */ new Set([
|
|
1971
|
+
"await_job",
|
|
1972
|
+
"get_job",
|
|
1973
|
+
"cancel_job",
|
|
1974
|
+
"check_setup",
|
|
1975
|
+
"setup_panel",
|
|
1976
|
+
"init_project",
|
|
1977
|
+
"ae_guide",
|
|
1978
|
+
"log_issue",
|
|
1979
|
+
"list_known_issues",
|
|
1980
|
+
"mark_issue_reported"
|
|
1981
|
+
]);
|
|
1982
|
+
var GUIDE_URI_PREFIX = "ae://guide/";
|
|
1474
1983
|
var AwaitJobSchema = schemas_exports.AwaitJob;
|
|
1475
1984
|
var GetJobSchema = schemas_exports.GetJob;
|
|
1476
1985
|
var CancelJobSchema = schemas_exports.CancelJob;
|
|
1477
1986
|
function createServer() {
|
|
1478
1987
|
const server = new Server(
|
|
1479
|
-
{ name: "after-effects-mcp", version: "0.
|
|
1480
|
-
{
|
|
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
|
+
}
|
|
1481
1996
|
);
|
|
1482
1997
|
const bridge = new HttpClient();
|
|
1483
1998
|
const jobs = new JobManager();
|
|
1484
1999
|
const ws = new WsClient(bridge.port, jobs);
|
|
1485
2000
|
ws.start();
|
|
2001
|
+
const panelGate = createPanelGate(bridge);
|
|
1486
2002
|
bridge.health().then(
|
|
1487
2003
|
(h) => logger.info(`Bridge healthy on port ${h.port}`),
|
|
1488
2004
|
(e) => logger.warn(`Bridge not reachable yet: ${e.message}`)
|
|
@@ -1502,6 +2018,41 @@ function createServer() {
|
|
|
1502
2018
|
});
|
|
1503
2019
|
return { tools };
|
|
1504
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
|
+
});
|
|
1505
2056
|
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
1506
2057
|
const name = req.params.name;
|
|
1507
2058
|
const rawArgs = req.params.arguments ?? {};
|
|
@@ -1533,8 +2084,41 @@ function createServer() {
|
|
|
1533
2084
|
if (name === "setup_panel") {
|
|
1534
2085
|
const a = schemas_exports.SetupPanel.parse(rawArgs);
|
|
1535
2086
|
const installed = await installPanel({ enableDebugMode: a.enableDebugMode, force: a.force });
|
|
2087
|
+
panelGate.invalidate();
|
|
1536
2088
|
return textResult({ ...installed, setup: await checkSetup() });
|
|
1537
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
|
+
}
|
|
2109
|
+
if (name === "log_issue") {
|
|
2110
|
+
const a = schemas_exports.LogIssue.parse(rawArgs);
|
|
2111
|
+
return textResult(logIssue(a));
|
|
2112
|
+
}
|
|
2113
|
+
if (name === "list_known_issues") {
|
|
2114
|
+
const a = schemas_exports.ListKnownIssues.parse(rawArgs);
|
|
2115
|
+
return textResult(listIssues(a.status ?? "all", a.tool));
|
|
2116
|
+
}
|
|
2117
|
+
if (name === "mark_issue_reported") {
|
|
2118
|
+
const a = schemas_exports.MarkIssueReported.parse(rawArgs);
|
|
2119
|
+
const entry = markReported(a.id, a.url);
|
|
2120
|
+
return textResult({ ok: true, id: entry.id, reported: true, issueUrl: entry.issueUrl });
|
|
2121
|
+
}
|
|
1538
2122
|
} catch (e) {
|
|
1539
2123
|
return errorResult(e.message);
|
|
1540
2124
|
}
|
|
@@ -1545,6 +2129,8 @@ function createServer() {
|
|
|
1545
2129
|
} catch (e) {
|
|
1546
2130
|
return errorResult(`Invalid arguments for ${name}: ${e.message}`);
|
|
1547
2131
|
}
|
|
2132
|
+
const staleness = await panelGate.check();
|
|
2133
|
+
if (staleness) return errorResult(staleness);
|
|
1548
2134
|
try {
|
|
1549
2135
|
const result = await bridge.runOp(name, args, progressToken);
|
|
1550
2136
|
if (ASYNC_OPS.has(name) && isAsyncEnvelope(result)) {
|
|
@@ -1583,12 +2169,54 @@ function createServer() {
|
|
|
1583
2169
|
return textResult(result);
|
|
1584
2170
|
} catch (e) {
|
|
1585
2171
|
if (e instanceof BridgeUnreachableError) return errorResult(e.message);
|
|
1586
|
-
if (e instanceof AeError)
|
|
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
|
+
}
|
|
1587
2179
|
return errorResult(e.message);
|
|
1588
2180
|
}
|
|
1589
2181
|
});
|
|
1590
2182
|
return server;
|
|
1591
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
|
+
}
|
|
1592
2220
|
function textResult(value) {
|
|
1593
2221
|
return {
|
|
1594
2222
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
@@ -1663,7 +2291,7 @@ ${USAGE}`);
|
|
|
1663
2291
|
await server.connect(transport);
|
|
1664
2292
|
logger.info("MCP server running on stdio");
|
|
1665
2293
|
}
|
|
1666
|
-
var VERSION = "0.
|
|
2294
|
+
var VERSION = "0.2.0";
|
|
1667
2295
|
main().catch((e) => {
|
|
1668
2296
|
logger.error("fatal", e.message);
|
|
1669
2297
|
process.exit(1);
|