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