@engine-room/after-effects-mcp 0.1.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/LICENSE +21 -0
- package/README.md +176 -0
- package/bin/server.js +1616 -0
- package/package.json +64 -0
- package/panel/CSXS/manifest.xml +49 -0
- package/panel/client/csinterface.js +32 -0
- package/panel/client/index.html +27 -0
- package/panel/client/main.js +371 -0
- package/panel/jsx/boot.jsx +3 -0
- package/panel/jsx/bundle.jsx +1562 -0
- package/panel/package.json +9 -0
package/bin/server.js
ADDED
|
@@ -0,0 +1,1616 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// src/index.ts
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
|
|
11
|
+
// src/cli/init.ts
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
var HOUSE_STYLE = `---
|
|
15
|
+
name: house-style
|
|
16
|
+
description: The visual and motion style for this project \u2014 palette, type, timing and layout rules. Load whenever building or editing anything in After Effects for this project.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# House style
|
|
20
|
+
|
|
21
|
+
<!--
|
|
22
|
+
This file is yours. Claude reads it before building anything in After Effects,
|
|
23
|
+
so whatever you write here becomes the default look of your work.
|
|
24
|
+
|
|
25
|
+
Replace the placeholders below with your own values. Be specific and concrete:
|
|
26
|
+
"dark navy #131521 at 92% opacity" is usable, "modern and clean" is not. If a
|
|
27
|
+
number matters \u2014 a corner radius, a stroke width, a hold duration \u2014 write the
|
|
28
|
+
number down.
|
|
29
|
+
|
|
30
|
+
The fastest way to fill this in: build one piece the way you like it, then ask
|
|
31
|
+
Claude "read this comp and write it up in my house-style skill".
|
|
32
|
+
-->
|
|
33
|
+
|
|
34
|
+
## Palette
|
|
35
|
+
|
|
36
|
+
| Role | Colour | Notes |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| Background | \`#000000\` | |
|
|
39
|
+
| Primary text | \`#FFFFFF\` | |
|
|
40
|
+
| Accent | \`#3DC46E\` | Used for emphasis and positive values |
|
|
41
|
+
| Warning / negative | \`#E03333\` | |
|
|
42
|
+
|
|
43
|
+
## Type
|
|
44
|
+
|
|
45
|
+
- **Headings:** _font name_, weight, size range
|
|
46
|
+
- **Body:** _font name_, weight, size range
|
|
47
|
+
- **Tracking / leading:** _your defaults_
|
|
48
|
+
- Text is left-aligned unless stated otherwise.
|
|
49
|
+
|
|
50
|
+
## Motion
|
|
51
|
+
|
|
52
|
+
- **Standard in:** scale 0 \u2192 108 \u2192 100 with easy ease, over ~0.4s
|
|
53
|
+
- **Standard out:** scale \u2192 0 over ~0.3s
|
|
54
|
+
- **Easing:** easy ease on everything; no linear motion unless mechanical
|
|
55
|
+
- **Idle life:** subtle \`wiggle()\` on position so nothing sits perfectly still
|
|
56
|
+
|
|
57
|
+
## Layout
|
|
58
|
+
|
|
59
|
+
- Comp size and frame rate: _e.g. 1920\xD71080 at 30fps_
|
|
60
|
+
- Safe margins: _e.g. 120px from every edge_
|
|
61
|
+
- Where elements sit by default: _e.g. lower third, left-aligned_
|
|
62
|
+
|
|
63
|
+
## Rules
|
|
64
|
+
|
|
65
|
+
- _Anything that should always or never happen. E.g. "never put text directly on
|
|
66
|
+
footage \u2014 always on a rounded chip", "keep total runtime under 8 seconds"._
|
|
67
|
+
`;
|
|
68
|
+
var CLAUDE_MD = `# {{NAME}}
|
|
69
|
+
|
|
70
|
+
After Effects project folder. Claude drives After Effects directly from here.
|
|
71
|
+
|
|
72
|
+
## How to work in this folder
|
|
73
|
+
|
|
74
|
+
1. Open After Effects with the project you want to work on.
|
|
75
|
+
2. Describe what you want in plain language \u2014 "build a lower third that says
|
|
76
|
+
Chapter One and slides in from the left".
|
|
77
|
+
3. Claude reads the current state of the comp, builds it, and shows you.
|
|
78
|
+
|
|
79
|
+
## Style
|
|
80
|
+
|
|
81
|
+
The look of everything built here is defined in
|
|
82
|
+
\`.claude/skills/house-style/SKILL.md\`. Edit that file to change the defaults \u2014
|
|
83
|
+
palette, type, timing, layout. It is read automatically.
|
|
84
|
+
|
|
85
|
+
## Conventions for this project
|
|
86
|
+
|
|
87
|
+
<!-- Anything specific to this project rather than to your general style:
|
|
88
|
+
naming conventions for comps and layers, delivery specs, the client's
|
|
89
|
+
requirements, what lives in which comp. -->
|
|
90
|
+
|
|
91
|
+
- Renders go in \`renders/\`.
|
|
92
|
+
`;
|
|
93
|
+
var MCP_JSON = `{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"after-effects": {
|
|
96
|
+
"command": "npx",
|
|
97
|
+
"args": ["-y", "@engine-room/after-effects-mcp"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
function parseInitArgs(argv) {
|
|
103
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
104
|
+
const withMcp = argv.includes("--with-mcp");
|
|
105
|
+
const unknown = argv.filter((a) => a.startsWith("-") && a !== "--with-mcp");
|
|
106
|
+
if (unknown.length > 0) return { error: `Unknown option: ${unknown[0]}` };
|
|
107
|
+
if (positional.length === 0) return { error: "Missing target directory." };
|
|
108
|
+
if (positional.length > 1) return { error: `Expected one directory, got ${positional.length}.` };
|
|
109
|
+
return { dir: positional[0], withMcp };
|
|
110
|
+
}
|
|
111
|
+
function runInit(argv) {
|
|
112
|
+
const parsed = parseInitArgs(argv);
|
|
113
|
+
if ("error" in parsed) {
|
|
114
|
+
process.stderr.write(`${parsed.error}
|
|
115
|
+
|
|
116
|
+
Usage: npx @engine-room/after-effects-mcp init <directory> [--with-mcp]
|
|
117
|
+
`);
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
const target = path.resolve(parsed.dir);
|
|
121
|
+
const name = path.basename(target);
|
|
122
|
+
const files = [
|
|
123
|
+
["CLAUDE.md", CLAUDE_MD.replace("{{NAME}}", name)],
|
|
124
|
+
[path.join(".claude", "skills", "house-style", "SKILL.md"), HOUSE_STYLE],
|
|
125
|
+
[path.join("renders", ".gitkeep"), ""]
|
|
126
|
+
];
|
|
127
|
+
if (parsed.withMcp) files.push([".mcp.json", MCP_JSON]);
|
|
128
|
+
const existing = files.map(([rel]) => rel).filter((rel) => fs.existsSync(path.join(target, rel)));
|
|
129
|
+
if (existing.length > 0) {
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
`Refusing to overwrite existing files in ${target}:
|
|
132
|
+
` + existing.map((f) => ` ${f}
|
|
133
|
+
`).join("") + `
|
|
134
|
+
Delete them first, or choose a different directory.
|
|
135
|
+
`
|
|
136
|
+
);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
for (const [rel, content] of files) {
|
|
140
|
+
const full = path.join(target, rel);
|
|
141
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
142
|
+
fs.writeFileSync(full, content, "utf8");
|
|
143
|
+
}
|
|
144
|
+
const out = [
|
|
145
|
+
`Created ${target}`,
|
|
146
|
+
``,
|
|
147
|
+
` CLAUDE.md what this project is`,
|
|
148
|
+
` .claude/skills/house-style/SKILL.md your look \u2014 edit this first`,
|
|
149
|
+
` renders/ exports land here`,
|
|
150
|
+
...parsed.withMcp ? [` .mcp.json connects Claude to After Effects`] : [],
|
|
151
|
+
``,
|
|
152
|
+
`Next:`,
|
|
153
|
+
` 1. Open the folder in Claude Code: claude ${parsed.dir}`,
|
|
154
|
+
` 2. Fill in .claude/skills/house-style/SKILL.md with your palette, type and timing.`,
|
|
155
|
+
` 3. Open After Effects, then ask for what you want.`,
|
|
156
|
+
...parsed.withMcp ? [] : [
|
|
157
|
+
``,
|
|
158
|
+
`This folder has no .mcp.json \u2014 it assumes you installed the Claude Code`,
|
|
159
|
+
`plugin, which already provides the After Effects tools. If you did not,`,
|
|
160
|
+
`re-run with --with-mcp to add a project-level connection instead.`
|
|
161
|
+
],
|
|
162
|
+
``
|
|
163
|
+
].join("\n");
|
|
164
|
+
process.stdout.write(out);
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/server.ts
|
|
169
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
170
|
+
import {
|
|
171
|
+
CallToolRequestSchema,
|
|
172
|
+
ListToolsRequestSchema
|
|
173
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
174
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
175
|
+
|
|
176
|
+
// ../shared/dist/ipc.js
|
|
177
|
+
import { z } from "zod";
|
|
178
|
+
var OpRequest = z.object({
|
|
179
|
+
op: z.string(),
|
|
180
|
+
args: z.unknown().optional(),
|
|
181
|
+
requestId: z.string(),
|
|
182
|
+
progressToken: z.union([z.string(), z.number()]).optional()
|
|
183
|
+
});
|
|
184
|
+
var OpSyncResponse = z.object({
|
|
185
|
+
ok: z.literal(true),
|
|
186
|
+
result: z.unknown()
|
|
187
|
+
});
|
|
188
|
+
var OpAsyncResponse = z.object({
|
|
189
|
+
ok: z.literal(true),
|
|
190
|
+
jobId: z.string(),
|
|
191
|
+
async: z.literal(true)
|
|
192
|
+
});
|
|
193
|
+
var OpErrorResponse = z.object({
|
|
194
|
+
ok: z.literal(false),
|
|
195
|
+
error: z.string(),
|
|
196
|
+
stack: z.string().optional(),
|
|
197
|
+
line: z.number().optional()
|
|
198
|
+
});
|
|
199
|
+
var OpResponse = z.union([OpSyncResponse, OpAsyncResponse, OpErrorResponse]);
|
|
200
|
+
var WsEvent = z.discriminatedUnion("type", [
|
|
201
|
+
z.object({
|
|
202
|
+
type: z.literal("progress"),
|
|
203
|
+
jobId: z.string(),
|
|
204
|
+
progress: z.number(),
|
|
205
|
+
total: z.number().optional(),
|
|
206
|
+
message: z.string().optional()
|
|
207
|
+
}),
|
|
208
|
+
z.object({
|
|
209
|
+
type: z.literal("complete"),
|
|
210
|
+
jobId: z.string(),
|
|
211
|
+
result: z.unknown()
|
|
212
|
+
}),
|
|
213
|
+
z.object({
|
|
214
|
+
type: z.literal("error"),
|
|
215
|
+
jobId: z.string(),
|
|
216
|
+
error: z.string(),
|
|
217
|
+
stack: z.string().optional()
|
|
218
|
+
}),
|
|
219
|
+
z.object({
|
|
220
|
+
type: z.literal("log"),
|
|
221
|
+
level: z.enum(["debug", "info", "warn", "error"]),
|
|
222
|
+
message: z.string()
|
|
223
|
+
})
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
// ../shared/dist/schemas.js
|
|
227
|
+
var schemas_exports = {};
|
|
228
|
+
__export(schemas_exports, {
|
|
229
|
+
AddEffect: () => AddEffect,
|
|
230
|
+
AddKeyframe: () => AddKeyframe,
|
|
231
|
+
AddMarker: () => AddMarker,
|
|
232
|
+
AddMask: () => AddMask,
|
|
233
|
+
AddShapeContent: () => AddShapeContent,
|
|
234
|
+
AddTextAnimator: () => AddTextAnimator,
|
|
235
|
+
AwaitJob: () => AwaitJob,
|
|
236
|
+
CancelJob: () => CancelJob,
|
|
237
|
+
CheckSetup: () => CheckSetup,
|
|
238
|
+
ClearExpression: () => ClearExpression,
|
|
239
|
+
Color: () => Color,
|
|
240
|
+
ColorRGBA: () => ColorRGBA,
|
|
241
|
+
CreateAdjustmentLayer: () => CreateAdjustmentLayer,
|
|
242
|
+
CreateCameraLayer: () => CreateCameraLayer,
|
|
243
|
+
CreateComp: () => CreateComp,
|
|
244
|
+
CreateLightLayer: () => CreateLightLayer,
|
|
245
|
+
CreateNullLayer: () => CreateNullLayer,
|
|
246
|
+
CreatePrecompLayer: () => CreatePrecompLayer,
|
|
247
|
+
CreateShapeLayer: () => CreateShapeLayer,
|
|
248
|
+
CreateSolidLayer: () => CreateSolidLayer,
|
|
249
|
+
CreateTextLayer: () => CreateTextLayer,
|
|
250
|
+
DeleteComp: () => DeleteComp,
|
|
251
|
+
DeleteLayer: () => DeleteLayer,
|
|
252
|
+
DuplicateLayer: () => DuplicateLayer,
|
|
253
|
+
FindLayers: () => FindLayers,
|
|
254
|
+
GetComp: () => GetComp,
|
|
255
|
+
GetCompTree: () => GetCompTree,
|
|
256
|
+
GetExpression: () => GetExpression,
|
|
257
|
+
GetJob: () => GetJob,
|
|
258
|
+
GetKeyframes: () => GetKeyframes,
|
|
259
|
+
GetLayerFull: () => GetLayerFull,
|
|
260
|
+
GetProjectSummary: () => GetProjectSummary,
|
|
261
|
+
Interpolation: () => Interpolation,
|
|
262
|
+
ListAvailableEffects: () => ListAvailableEffects,
|
|
263
|
+
ListComps: () => ListComps,
|
|
264
|
+
ListEffects: () => ListEffects,
|
|
265
|
+
ListLayers: () => ListLayers,
|
|
266
|
+
OpSchemas: () => OpSchemas,
|
|
267
|
+
ParentLayer: () => ParentLayer,
|
|
268
|
+
PropertyPath: () => PropertyPath,
|
|
269
|
+
RemoveEffect: () => RemoveEffect,
|
|
270
|
+
RemoveKeyframe: () => RemoveKeyframe,
|
|
271
|
+
RemoveMarker: () => RemoveMarker,
|
|
272
|
+
RemoveMask: () => RemoveMask,
|
|
273
|
+
ReorderLayer: () => ReorderLayer,
|
|
274
|
+
RunBatch: () => RunBatch,
|
|
275
|
+
RunJsx: () => RunJsx,
|
|
276
|
+
ScreenshotFrame: () => ScreenshotFrame,
|
|
277
|
+
ScreenshotLayer: () => ScreenshotLayer,
|
|
278
|
+
SetActiveComp: () => SetActiveComp,
|
|
279
|
+
SetComp: () => SetComp,
|
|
280
|
+
SetEffectEnabled: () => SetEffectEnabled,
|
|
281
|
+
SetEffectParam: () => SetEffectParam,
|
|
282
|
+
SetExpression: () => SetExpression,
|
|
283
|
+
SetInterpolation: () => SetInterpolation,
|
|
284
|
+
SetLayer: () => SetLayer,
|
|
285
|
+
SetMask: () => SetMask,
|
|
286
|
+
SetShapePath: () => SetShapePath,
|
|
287
|
+
SetShapeProperty: () => SetShapeProperty,
|
|
288
|
+
SetSpatialTangents: () => SetSpatialTangents,
|
|
289
|
+
SetTemporalEase: () => SetTemporalEase,
|
|
290
|
+
SetText: () => SetText,
|
|
291
|
+
SetTransform: () => SetTransform,
|
|
292
|
+
SetupPanel: () => SetupPanel,
|
|
293
|
+
ShapeContent: () => ShapeContent,
|
|
294
|
+
ToggleExpression: () => ToggleExpression,
|
|
295
|
+
Vec2: () => Vec2,
|
|
296
|
+
Vec3: () => Vec3,
|
|
297
|
+
VecAny: () => VecAny
|
|
298
|
+
});
|
|
299
|
+
import { z as z2 } from "zod";
|
|
300
|
+
var Color = z2.tuple([z2.number(), z2.number(), z2.number()]).describe("RGB 0..1");
|
|
301
|
+
var ColorRGBA = z2.tuple([z2.number(), z2.number(), z2.number(), z2.number()]);
|
|
302
|
+
var Vec2 = z2.tuple([z2.number(), z2.number()]);
|
|
303
|
+
var Vec3 = z2.tuple([z2.number(), z2.number(), z2.number()]);
|
|
304
|
+
var VecAny = z2.union([z2.number(), Vec2, Vec3, z2.array(z2.number())]);
|
|
305
|
+
var PropertyPath = z2.array(z2.union([z2.string(), z2.number()])).min(1).describe("Dotted property path into a layer, e.g. ['Transform','Position'] or ['Effects','Gaussian Blur','Blurriness'].");
|
|
306
|
+
var Interpolation = z2.object({
|
|
307
|
+
in: z2.enum(["linear", "bezier", "hold"]).optional(),
|
|
308
|
+
out: z2.enum(["linear", "bezier", "hold"]).optional(),
|
|
309
|
+
easeIn: z2.object({ influence: z2.number(), speed: z2.number() }).optional(),
|
|
310
|
+
easeOut: z2.object({ influence: z2.number(), speed: z2.number() }).optional()
|
|
311
|
+
});
|
|
312
|
+
var ListComps = z2.object({}).strict();
|
|
313
|
+
var GetComp = z2.object({ compId: z2.number() });
|
|
314
|
+
var GetCompTree = z2.object({ compId: z2.number(), depth: z2.number().int().min(0).max(8).default(2).optional() });
|
|
315
|
+
var CreateComp = z2.object({
|
|
316
|
+
name: z2.string().default("Untitled"),
|
|
317
|
+
width: z2.number().int().positive().default(1920),
|
|
318
|
+
height: z2.number().int().positive().default(1080),
|
|
319
|
+
frameRate: z2.number().positive().default(30),
|
|
320
|
+
duration: z2.number().positive().default(5),
|
|
321
|
+
pixelAspect: z2.number().positive().default(1).optional(),
|
|
322
|
+
bgColor: Color.optional()
|
|
323
|
+
});
|
|
324
|
+
var SetComp = z2.object({
|
|
325
|
+
compId: z2.number(),
|
|
326
|
+
name: z2.string().optional(),
|
|
327
|
+
width: z2.number().int().positive().optional(),
|
|
328
|
+
height: z2.number().int().positive().optional(),
|
|
329
|
+
frameRate: z2.number().positive().optional(),
|
|
330
|
+
duration: z2.number().positive().optional(),
|
|
331
|
+
workAreaStart: z2.number().optional(),
|
|
332
|
+
workAreaDuration: z2.number().positive().optional(),
|
|
333
|
+
bgColor: Color.optional()
|
|
334
|
+
});
|
|
335
|
+
var DeleteComp = z2.object({ compId: z2.number() });
|
|
336
|
+
var SetActiveComp = z2.object({ compId: z2.number() });
|
|
337
|
+
var ListLayers = z2.object({ compId: z2.number() });
|
|
338
|
+
var GetLayerFull = z2.object({ compId: z2.number(), layerId: z2.number(), includeChildren: z2.boolean().default(false).optional() });
|
|
339
|
+
var CreateTextLayer = z2.object({
|
|
340
|
+
compId: z2.number(),
|
|
341
|
+
text: z2.string().default(""),
|
|
342
|
+
font: z2.string().optional(),
|
|
343
|
+
size: z2.number().positive().optional(),
|
|
344
|
+
color: Color.optional(),
|
|
345
|
+
position: VecAny.optional(),
|
|
346
|
+
anchorAlign: z2.enum(["left", "center", "right", "none"]).default("left").optional().describe("Where the anchor point sits on the rendered text. Default 'left' makes `position` mean the visible left edge of the first character's baseline \u2014 the natural assumption. AE's addText() defaults to a centered anchor, which surprises agents; use 'none' to keep AE's raw default."),
|
|
347
|
+
name: z2.string().optional()
|
|
348
|
+
});
|
|
349
|
+
var CreateShapeLayer = z2.object({
|
|
350
|
+
compId: z2.number(),
|
|
351
|
+
name: z2.string().optional(),
|
|
352
|
+
shapes: z2.array(z2.record(z2.string(), z2.unknown())).default([]),
|
|
353
|
+
fill: Color.optional(),
|
|
354
|
+
stroke: Color.optional(),
|
|
355
|
+
strokeWidth: z2.number().nonnegative().optional()
|
|
356
|
+
});
|
|
357
|
+
var CreateSolidLayer = z2.object({
|
|
358
|
+
compId: z2.number(),
|
|
359
|
+
name: z2.string().optional(),
|
|
360
|
+
color: Color,
|
|
361
|
+
width: z2.number().int().positive().optional(),
|
|
362
|
+
height: z2.number().int().positive().optional(),
|
|
363
|
+
duration: z2.number().positive().optional()
|
|
364
|
+
});
|
|
365
|
+
var CreateNullLayer = z2.object({ compId: z2.number(), name: z2.string().optional() });
|
|
366
|
+
var CreateAdjustmentLayer = z2.object({ compId: z2.number(), name: z2.string().optional() });
|
|
367
|
+
var CreatePrecompLayer = z2.object({ compId: z2.number(), sourceCompId: z2.number(), position: VecAny.optional() });
|
|
368
|
+
var CreateCameraLayer = z2.object({ compId: z2.number(), name: z2.string().optional(), oneNode: z2.boolean().default(false), position: VecAny.optional() });
|
|
369
|
+
var CreateLightLayer = z2.object({
|
|
370
|
+
compId: z2.number(),
|
|
371
|
+
name: z2.string().optional(),
|
|
372
|
+
lightType: z2.enum(["parallel", "spot", "point", "ambient"]).default("point"),
|
|
373
|
+
color: Color.optional(),
|
|
374
|
+
intensity: z2.number().optional(),
|
|
375
|
+
position: VecAny.optional()
|
|
376
|
+
});
|
|
377
|
+
var DuplicateLayer = z2.object({ compId: z2.number(), layerId: z2.number(), count: z2.number().int().positive().default(1).optional() });
|
|
378
|
+
var DeleteLayer = z2.object({ compId: z2.number(), layerId: z2.number() });
|
|
379
|
+
var SetLayer = z2.object({
|
|
380
|
+
compId: z2.number(),
|
|
381
|
+
layerId: z2.number(),
|
|
382
|
+
name: z2.string().optional(),
|
|
383
|
+
enabled: z2.boolean().optional(),
|
|
384
|
+
locked: z2.boolean().optional(),
|
|
385
|
+
shy: z2.boolean().optional(),
|
|
386
|
+
solo: z2.boolean().optional(),
|
|
387
|
+
threeDLayer: z2.boolean().optional(),
|
|
388
|
+
blendingMode: z2.string().optional(),
|
|
389
|
+
label: z2.number().int().min(0).max(16).optional(),
|
|
390
|
+
inPoint: z2.number().optional(),
|
|
391
|
+
outPoint: z2.number().optional(),
|
|
392
|
+
startTime: z2.number().optional(),
|
|
393
|
+
stretch: z2.number().optional(),
|
|
394
|
+
preserveTransparency: z2.boolean().optional(),
|
|
395
|
+
trackMatte: z2.object({ type: z2.string(), layerId: z2.number().optional() }).optional()
|
|
396
|
+
});
|
|
397
|
+
var ParentLayer = z2.object({ compId: z2.number(), layerId: z2.number(), parentLayerId: z2.number().nullable() });
|
|
398
|
+
var ReorderLayer = z2.object({ compId: z2.number(), layerId: z2.number(), toIndex: z2.number().int().positive() });
|
|
399
|
+
var SetTransform = z2.object({
|
|
400
|
+
compId: z2.number(),
|
|
401
|
+
layerId: z2.number(),
|
|
402
|
+
time: z2.number().optional(),
|
|
403
|
+
keyframe: z2.boolean().default(false).optional(),
|
|
404
|
+
properties: z2.object({
|
|
405
|
+
position: VecAny.optional(),
|
|
406
|
+
scale: VecAny.optional(),
|
|
407
|
+
rotation: z2.number().optional(),
|
|
408
|
+
anchorPoint: VecAny.optional(),
|
|
409
|
+
opacity: z2.number().min(0).max(100).optional(),
|
|
410
|
+
orientation: Vec3.optional(),
|
|
411
|
+
xRotation: z2.number().optional(),
|
|
412
|
+
yRotation: z2.number().optional(),
|
|
413
|
+
zRotation: z2.number().optional()
|
|
414
|
+
})
|
|
415
|
+
});
|
|
416
|
+
var AddKeyframe = z2.object({
|
|
417
|
+
compId: z2.number(),
|
|
418
|
+
layerId: z2.number(),
|
|
419
|
+
propertyPath: PropertyPath,
|
|
420
|
+
time: z2.number(),
|
|
421
|
+
value: VecAny,
|
|
422
|
+
interpolation: Interpolation.optional()
|
|
423
|
+
});
|
|
424
|
+
var RemoveKeyframe = z2.object({
|
|
425
|
+
compId: z2.number(),
|
|
426
|
+
layerId: z2.number(),
|
|
427
|
+
propertyPath: PropertyPath,
|
|
428
|
+
time: z2.number()
|
|
429
|
+
});
|
|
430
|
+
var GetKeyframes = z2.object({
|
|
431
|
+
compId: z2.number(),
|
|
432
|
+
layerId: z2.number(),
|
|
433
|
+
propertyPath: PropertyPath
|
|
434
|
+
});
|
|
435
|
+
var SetInterpolation = z2.object({
|
|
436
|
+
compId: z2.number(),
|
|
437
|
+
layerId: z2.number(),
|
|
438
|
+
propertyPath: PropertyPath,
|
|
439
|
+
keyIndex: z2.number().int().positive(),
|
|
440
|
+
in: z2.enum(["linear", "bezier", "hold"]).optional(),
|
|
441
|
+
out: z2.enum(["linear", "bezier", "hold"]).optional()
|
|
442
|
+
});
|
|
443
|
+
var SetTemporalEase = z2.object({
|
|
444
|
+
compId: z2.number(),
|
|
445
|
+
layerId: z2.number(),
|
|
446
|
+
propertyPath: PropertyPath,
|
|
447
|
+
keyIndex: z2.number().int().positive(),
|
|
448
|
+
easeIn: z2.object({ influence: z2.number(), speed: z2.number() }).optional(),
|
|
449
|
+
easeOut: z2.object({ influence: z2.number(), speed: z2.number() }).optional()
|
|
450
|
+
});
|
|
451
|
+
var SetSpatialTangents = z2.object({
|
|
452
|
+
compId: z2.number(),
|
|
453
|
+
layerId: z2.number(),
|
|
454
|
+
propertyPath: PropertyPath,
|
|
455
|
+
keyIndex: z2.number().int().positive(),
|
|
456
|
+
inTangent: z2.array(z2.number()).min(2).max(3),
|
|
457
|
+
outTangent: z2.array(z2.number()).min(2).max(3)
|
|
458
|
+
});
|
|
459
|
+
var GetExpression = z2.object({ compId: z2.number(), layerId: z2.number(), propertyPath: PropertyPath });
|
|
460
|
+
var SetExpression = z2.object({ compId: z2.number(), layerId: z2.number(), propertyPath: PropertyPath, expression: z2.string() });
|
|
461
|
+
var ToggleExpression = z2.object({ compId: z2.number(), layerId: z2.number(), propertyPath: PropertyPath, enabled: z2.boolean() });
|
|
462
|
+
var ClearExpression = z2.object({ compId: z2.number(), layerId: z2.number(), propertyPath: PropertyPath });
|
|
463
|
+
var ListEffects = z2.object({ compId: z2.number(), layerId: z2.number() });
|
|
464
|
+
var AddEffect = z2.object({ compId: z2.number(), layerId: z2.number(), matchName: z2.string() });
|
|
465
|
+
var RemoveEffect = z2.object({ compId: z2.number(), layerId: z2.number(), effectIndex: z2.number().int().positive() });
|
|
466
|
+
var SetEffectParam = z2.object({
|
|
467
|
+
compId: z2.number(),
|
|
468
|
+
layerId: z2.number(),
|
|
469
|
+
effectIndex: z2.number().int().positive(),
|
|
470
|
+
paramName: z2.string().optional(),
|
|
471
|
+
paramMatchName: z2.string().optional(),
|
|
472
|
+
value: VecAny,
|
|
473
|
+
time: z2.number().optional(),
|
|
474
|
+
keyframe: z2.boolean().default(false).optional()
|
|
475
|
+
});
|
|
476
|
+
var SetEffectEnabled = z2.object({ compId: z2.number(), layerId: z2.number(), effectIndex: z2.number().int().positive(), enabled: z2.boolean() });
|
|
477
|
+
var ListAvailableEffects = z2.object({}).strict();
|
|
478
|
+
var SetText = z2.object({
|
|
479
|
+
compId: z2.number(),
|
|
480
|
+
layerId: z2.number(),
|
|
481
|
+
text: z2.string().optional(),
|
|
482
|
+
font: z2.string().optional(),
|
|
483
|
+
size: z2.number().positive().optional(),
|
|
484
|
+
fillColor: Color.optional(),
|
|
485
|
+
strokeColor: Color.optional(),
|
|
486
|
+
strokeWidth: z2.number().nonnegative().optional(),
|
|
487
|
+
tracking: z2.number().optional(),
|
|
488
|
+
leading: z2.number().optional(),
|
|
489
|
+
justification: z2.enum(["left", "center", "right", "full"]).optional(),
|
|
490
|
+
applyFill: z2.boolean().optional(),
|
|
491
|
+
applyStroke: z2.boolean().optional(),
|
|
492
|
+
fauxBold: z2.boolean().optional(),
|
|
493
|
+
fauxItalic: z2.boolean().optional(),
|
|
494
|
+
allCaps: z2.boolean().optional(),
|
|
495
|
+
smallCaps: z2.boolean().optional(),
|
|
496
|
+
baselineShift: z2.number().optional()
|
|
497
|
+
});
|
|
498
|
+
var AddTextAnimator = z2.object({
|
|
499
|
+
compId: z2.number(),
|
|
500
|
+
layerId: z2.number(),
|
|
501
|
+
type: z2.enum(["position", "scale", "rotation", "opacity", "tracking", "skew", "fillColor", "strokeColor"]),
|
|
502
|
+
range: z2.object({ start: z2.number().default(0), end: z2.number().default(100), offset: z2.number().default(0) }).optional()
|
|
503
|
+
});
|
|
504
|
+
var SetShapePath = z2.object({
|
|
505
|
+
compId: z2.number(),
|
|
506
|
+
layerId: z2.number(),
|
|
507
|
+
shapePath: PropertyPath,
|
|
508
|
+
vertices: z2.array(Vec2),
|
|
509
|
+
inTangents: z2.array(Vec2).optional(),
|
|
510
|
+
outTangents: z2.array(Vec2).optional(),
|
|
511
|
+
closed: z2.boolean().default(true).optional()
|
|
512
|
+
});
|
|
513
|
+
var ShapeColor = z2.union([Color, ColorRGBA]);
|
|
514
|
+
var ShapeContent = z2.discriminatedUnion("type", [
|
|
515
|
+
z2.object({
|
|
516
|
+
type: z2.literal("rect"),
|
|
517
|
+
name: z2.string().optional(),
|
|
518
|
+
size: Vec2.optional(),
|
|
519
|
+
position: Vec2.optional(),
|
|
520
|
+
roundness: z2.number().optional()
|
|
521
|
+
}).strict(),
|
|
522
|
+
z2.object({
|
|
523
|
+
type: z2.literal("ellipse"),
|
|
524
|
+
name: z2.string().optional(),
|
|
525
|
+
size: Vec2.optional(),
|
|
526
|
+
position: Vec2.optional()
|
|
527
|
+
}).strict(),
|
|
528
|
+
z2.object({
|
|
529
|
+
type: z2.literal("star"),
|
|
530
|
+
name: z2.string().optional(),
|
|
531
|
+
starType: z2.number().optional().describe("1 = star, 2 = polygon"),
|
|
532
|
+
points: z2.number().optional().describe("Number of points/sides"),
|
|
533
|
+
position: Vec2.optional(),
|
|
534
|
+
rotation: z2.number().optional(),
|
|
535
|
+
innerRadius: z2.number().optional(),
|
|
536
|
+
outerRadius: z2.number().optional(),
|
|
537
|
+
innerRoundness: z2.number().optional(),
|
|
538
|
+
outerRoundness: z2.number().optional()
|
|
539
|
+
}).strict(),
|
|
540
|
+
z2.object({
|
|
541
|
+
type: z2.literal("path"),
|
|
542
|
+
name: z2.string().optional(),
|
|
543
|
+
vertices: z2.array(Vec2).optional().describe("Bezier vertices in layer space"),
|
|
544
|
+
inTangents: z2.array(Vec2).optional(),
|
|
545
|
+
outTangents: z2.array(Vec2).optional(),
|
|
546
|
+
closed: z2.boolean().optional()
|
|
547
|
+
}).strict(),
|
|
548
|
+
z2.object({
|
|
549
|
+
type: z2.literal("fill"),
|
|
550
|
+
name: z2.string().optional(),
|
|
551
|
+
color: ShapeColor.optional(),
|
|
552
|
+
opacity: z2.number().optional().describe("0..100"),
|
|
553
|
+
fillRule: z2.number().optional()
|
|
554
|
+
}).strict(),
|
|
555
|
+
z2.object({
|
|
556
|
+
type: z2.literal("stroke"),
|
|
557
|
+
name: z2.string().optional(),
|
|
558
|
+
color: ShapeColor.optional(),
|
|
559
|
+
opacity: z2.number().optional().describe("0..100"),
|
|
560
|
+
width: z2.number().optional(),
|
|
561
|
+
lineCap: z2.number().optional(),
|
|
562
|
+
lineJoin: z2.number().optional(),
|
|
563
|
+
miterLimit: z2.number().optional()
|
|
564
|
+
}).strict(),
|
|
565
|
+
z2.object({
|
|
566
|
+
type: z2.literal("trim"),
|
|
567
|
+
name: z2.string().optional(),
|
|
568
|
+
start: z2.number().optional().describe("0..100"),
|
|
569
|
+
end: z2.number().optional().describe("0..100"),
|
|
570
|
+
offset: z2.number().optional()
|
|
571
|
+
}).strict(),
|
|
572
|
+
z2.object({
|
|
573
|
+
type: z2.literal("repeater"),
|
|
574
|
+
name: z2.string().optional(),
|
|
575
|
+
copies: z2.number().optional(),
|
|
576
|
+
offset: z2.number().optional()
|
|
577
|
+
}).strict(),
|
|
578
|
+
z2.object({
|
|
579
|
+
type: z2.literal("merge"),
|
|
580
|
+
name: z2.string().optional(),
|
|
581
|
+
mode: z2.number().optional()
|
|
582
|
+
}).strict(),
|
|
583
|
+
z2.object({
|
|
584
|
+
type: z2.literal("group"),
|
|
585
|
+
name: z2.string().optional()
|
|
586
|
+
}).strict()
|
|
587
|
+
]);
|
|
588
|
+
var AddShapeContent = z2.object({
|
|
589
|
+
compId: z2.number(),
|
|
590
|
+
layerId: z2.number(),
|
|
591
|
+
parentGroupPath: PropertyPath.optional(),
|
|
592
|
+
content: ShapeContent
|
|
593
|
+
});
|
|
594
|
+
var SetShapeProperty = z2.object({
|
|
595
|
+
compId: z2.number(),
|
|
596
|
+
layerId: z2.number(),
|
|
597
|
+
contentPath: PropertyPath,
|
|
598
|
+
property: z2.string(),
|
|
599
|
+
value: VecAny,
|
|
600
|
+
time: z2.number().optional(),
|
|
601
|
+
keyframe: z2.boolean().default(false).optional()
|
|
602
|
+
});
|
|
603
|
+
var AddMask = z2.object({
|
|
604
|
+
compId: z2.number(),
|
|
605
|
+
layerId: z2.number(),
|
|
606
|
+
vertices: z2.array(Vec2),
|
|
607
|
+
inTangents: z2.array(Vec2).optional(),
|
|
608
|
+
outTangents: z2.array(Vec2).optional(),
|
|
609
|
+
closed: z2.boolean().default(true).optional(),
|
|
610
|
+
mode: z2.string().default("ADD").optional()
|
|
611
|
+
});
|
|
612
|
+
var SetMask = z2.object({
|
|
613
|
+
compId: z2.number(),
|
|
614
|
+
layerId: z2.number(),
|
|
615
|
+
maskIndex: z2.number().int().positive(),
|
|
616
|
+
vertices: z2.array(Vec2).optional(),
|
|
617
|
+
inTangents: z2.array(Vec2).optional(),
|
|
618
|
+
outTangents: z2.array(Vec2).optional(),
|
|
619
|
+
closed: z2.boolean().optional(),
|
|
620
|
+
mode: z2.string().optional(),
|
|
621
|
+
inverted: z2.boolean().optional(),
|
|
622
|
+
expansion: z2.number().optional(),
|
|
623
|
+
feather: Vec2.optional(),
|
|
624
|
+
opacity: z2.number().optional()
|
|
625
|
+
});
|
|
626
|
+
var RemoveMask = z2.object({ compId: z2.number(), layerId: z2.number(), maskIndex: z2.number().int().positive() });
|
|
627
|
+
var AddMarker = z2.object({
|
|
628
|
+
compId: z2.number(),
|
|
629
|
+
layerId: z2.number().optional(),
|
|
630
|
+
time: z2.number(),
|
|
631
|
+
duration: z2.number().nonnegative().default(0).optional(),
|
|
632
|
+
comment: z2.string().optional(),
|
|
633
|
+
label: z2.number().int().min(0).max(16).optional(),
|
|
634
|
+
chapter: z2.string().optional(),
|
|
635
|
+
url: z2.string().optional(),
|
|
636
|
+
frameTarget: z2.string().optional()
|
|
637
|
+
});
|
|
638
|
+
var RemoveMarker = z2.object({
|
|
639
|
+
compId: z2.number(),
|
|
640
|
+
layerId: z2.number().optional(),
|
|
641
|
+
markerIndex: z2.number().int().positive()
|
|
642
|
+
});
|
|
643
|
+
var ScreenshotFrame = z2.object({
|
|
644
|
+
compId: z2.number(),
|
|
645
|
+
time: z2.number().optional(),
|
|
646
|
+
downsample: z2.number().int().min(1).max(8).default(1).optional()
|
|
647
|
+
});
|
|
648
|
+
var ScreenshotLayer = z2.object({
|
|
649
|
+
compId: z2.number(),
|
|
650
|
+
layerId: z2.number(),
|
|
651
|
+
time: z2.number().optional(),
|
|
652
|
+
downsample: z2.number().int().min(1).max(8).default(1).optional()
|
|
653
|
+
});
|
|
654
|
+
var RunBatch = z2.object({
|
|
655
|
+
ops: z2.array(z2.object({ op: z2.string(), args: z2.unknown() })),
|
|
656
|
+
transactional: z2.boolean().default(true).optional(),
|
|
657
|
+
undoGroupName: z2.string().default("AE MCP Batch").optional()
|
|
658
|
+
});
|
|
659
|
+
var GetProjectSummary = z2.object({}).strict();
|
|
660
|
+
var FindLayers = z2.object({
|
|
661
|
+
compId: z2.number().optional(),
|
|
662
|
+
namePattern: z2.string().optional(),
|
|
663
|
+
type: z2.string().optional(),
|
|
664
|
+
hasEffectMatchName: z2.string().optional()
|
|
665
|
+
});
|
|
666
|
+
var RunJsx = z2.object({ code: z2.string() });
|
|
667
|
+
var CheckSetup = z2.object({}).strict();
|
|
668
|
+
var SetupPanel = z2.object({
|
|
669
|
+
enableDebugMode: z2.boolean().default(true).optional().describe("Also enable Adobe's PlayerDebugMode preference, which AE requires to load this unsigned panel. Default true."),
|
|
670
|
+
force: z2.boolean().default(false).optional().describe("Replace an existing symlinked (development) install with a copy. Default false.")
|
|
671
|
+
}).strict();
|
|
672
|
+
var AwaitJob = z2.object({ jobId: z2.string(), timeoutMs: z2.number().int().positive().default(6e5).optional() });
|
|
673
|
+
var GetJob = z2.object({ jobId: z2.string() });
|
|
674
|
+
var CancelJob = z2.object({ jobId: z2.string() });
|
|
675
|
+
var OpSchemas = {
|
|
676
|
+
// comps
|
|
677
|
+
list_comps: ListComps,
|
|
678
|
+
get_comp: GetComp,
|
|
679
|
+
get_comp_tree: GetCompTree,
|
|
680
|
+
create_comp: CreateComp,
|
|
681
|
+
set_comp: SetComp,
|
|
682
|
+
delete_comp: DeleteComp,
|
|
683
|
+
set_active_comp: SetActiveComp,
|
|
684
|
+
// layers
|
|
685
|
+
list_layers: ListLayers,
|
|
686
|
+
get_layer_full: GetLayerFull,
|
|
687
|
+
create_text_layer: CreateTextLayer,
|
|
688
|
+
create_shape_layer: CreateShapeLayer,
|
|
689
|
+
create_solid_layer: CreateSolidLayer,
|
|
690
|
+
create_null_layer: CreateNullLayer,
|
|
691
|
+
create_adjustment_layer: CreateAdjustmentLayer,
|
|
692
|
+
create_precomp_layer: CreatePrecompLayer,
|
|
693
|
+
create_camera_layer: CreateCameraLayer,
|
|
694
|
+
create_light_layer: CreateLightLayer,
|
|
695
|
+
duplicate_layer: DuplicateLayer,
|
|
696
|
+
delete_layer: DeleteLayer,
|
|
697
|
+
set_layer: SetLayer,
|
|
698
|
+
parent_layer: ParentLayer,
|
|
699
|
+
reorder_layer: ReorderLayer,
|
|
700
|
+
// transforms
|
|
701
|
+
set_transform: SetTransform,
|
|
702
|
+
// keyframes
|
|
703
|
+
add_keyframe: AddKeyframe,
|
|
704
|
+
remove_keyframe: RemoveKeyframe,
|
|
705
|
+
get_keyframes: GetKeyframes,
|
|
706
|
+
set_interpolation: SetInterpolation,
|
|
707
|
+
set_temporal_ease: SetTemporalEase,
|
|
708
|
+
set_spatial_tangents: SetSpatialTangents,
|
|
709
|
+
// expressions
|
|
710
|
+
get_expression: GetExpression,
|
|
711
|
+
set_expression: SetExpression,
|
|
712
|
+
toggle_expression: ToggleExpression,
|
|
713
|
+
clear_expression: ClearExpression,
|
|
714
|
+
// effects
|
|
715
|
+
list_effects: ListEffects,
|
|
716
|
+
add_effect: AddEffect,
|
|
717
|
+
remove_effect: RemoveEffect,
|
|
718
|
+
set_effect_param: SetEffectParam,
|
|
719
|
+
set_effect_enabled: SetEffectEnabled,
|
|
720
|
+
list_available_effects: ListAvailableEffects,
|
|
721
|
+
// text
|
|
722
|
+
set_text: SetText,
|
|
723
|
+
add_text_animator: AddTextAnimator,
|
|
724
|
+
// shapes
|
|
725
|
+
set_shape_path: SetShapePath,
|
|
726
|
+
add_shape_content: AddShapeContent,
|
|
727
|
+
set_shape_property: SetShapeProperty,
|
|
728
|
+
// masks
|
|
729
|
+
add_mask: AddMask,
|
|
730
|
+
set_mask: SetMask,
|
|
731
|
+
remove_mask: RemoveMask,
|
|
732
|
+
// markers
|
|
733
|
+
add_marker: AddMarker,
|
|
734
|
+
remove_marker: RemoveMarker,
|
|
735
|
+
// vision
|
|
736
|
+
screenshot_frame: ScreenshotFrame,
|
|
737
|
+
screenshot_layer: ScreenshotLayer,
|
|
738
|
+
// batch
|
|
739
|
+
run_batch: RunBatch,
|
|
740
|
+
// explore
|
|
741
|
+
get_project_summary: GetProjectSummary,
|
|
742
|
+
find_layers: FindLayers,
|
|
743
|
+
// raw
|
|
744
|
+
run_jsx: RunJsx,
|
|
745
|
+
// jobs
|
|
746
|
+
await_job: AwaitJob,
|
|
747
|
+
get_job: GetJob,
|
|
748
|
+
cancel_job: CancelJob,
|
|
749
|
+
// setup
|
|
750
|
+
check_setup: CheckSetup,
|
|
751
|
+
setup_panel: SetupPanel
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// src/util/errors.ts
|
|
755
|
+
var BridgeUnreachableError = class _BridgeUnreachableError extends Error {
|
|
756
|
+
constructor(port, cause) {
|
|
757
|
+
super(_BridgeUnreachableError.message(port, cause));
|
|
758
|
+
this.port = port;
|
|
759
|
+
this.name = "BridgeUnreachableError";
|
|
760
|
+
}
|
|
761
|
+
port;
|
|
762
|
+
static message(port, cause) {
|
|
763
|
+
return [
|
|
764
|
+
`Cannot reach the After Effects panel at http://127.0.0.1:${port}.`,
|
|
765
|
+
cause ? `Underlying error: ${cause.message}` : "",
|
|
766
|
+
"",
|
|
767
|
+
"Call the check_setup tool to find out why, then relay its nextSteps to the user.",
|
|
768
|
+
"In most cases the fix is either running setup_panel (installs the panel) or",
|
|
769
|
+
"simply opening After Effects."
|
|
770
|
+
].filter(Boolean).join("\n");
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
var AeError = class extends Error {
|
|
774
|
+
constructor(message, stack_, line) {
|
|
775
|
+
super(message);
|
|
776
|
+
this.stack_ = stack_;
|
|
777
|
+
this.line = line;
|
|
778
|
+
this.name = "AeError";
|
|
779
|
+
}
|
|
780
|
+
stack_;
|
|
781
|
+
line;
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// src/util/logger.ts
|
|
785
|
+
var logger = {
|
|
786
|
+
info: (...a) => console.error("[ae-mcp]", ...a),
|
|
787
|
+
warn: (...a) => console.error("[ae-mcp:warn]", ...a),
|
|
788
|
+
error: (...a) => console.error("[ae-mcp:error]", ...a),
|
|
789
|
+
debug: (...a) => {
|
|
790
|
+
if (process.env.AE_MCP_DEBUG) console.error("[ae-mcp:debug]", ...a);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// src/bridge/discovery.ts
|
|
795
|
+
import fs2 from "node:fs";
|
|
796
|
+
import os from "node:os";
|
|
797
|
+
import path2 from "node:path";
|
|
798
|
+
var DEFAULT_PORT = 7777;
|
|
799
|
+
function discoverPort() {
|
|
800
|
+
const envPort = process.env.AE_MCP_PORT;
|
|
801
|
+
if (envPort) {
|
|
802
|
+
const n = parseInt(envPort, 10);
|
|
803
|
+
if (Number.isFinite(n)) return n;
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
const f = path2.join(os.homedir(), ".engineroom-ae-mcp", "port");
|
|
807
|
+
if (fs2.existsSync(f)) {
|
|
808
|
+
const txt = fs2.readFileSync(f, "utf8").trim();
|
|
809
|
+
const n = parseInt(txt, 10);
|
|
810
|
+
if (Number.isFinite(n)) return n;
|
|
811
|
+
}
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
return DEFAULT_PORT;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/bridge/httpClient.ts
|
|
818
|
+
var HttpClient = class {
|
|
819
|
+
port;
|
|
820
|
+
base;
|
|
821
|
+
constructor(port) {
|
|
822
|
+
this.port = port ?? discoverPort();
|
|
823
|
+
this.base = `http://127.0.0.1:${this.port}`;
|
|
824
|
+
}
|
|
825
|
+
async health() {
|
|
826
|
+
try {
|
|
827
|
+
const r = await fetch(`${this.base}/health`, { signal: AbortSignal.timeout(2e3) });
|
|
828
|
+
if (!r.ok) throw new Error(`health HTTP ${r.status}`);
|
|
829
|
+
return await r.json();
|
|
830
|
+
} catch (e) {
|
|
831
|
+
throw new BridgeUnreachableError(this.port, e);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async runOp(op, args, progressToken) {
|
|
835
|
+
let resp;
|
|
836
|
+
try {
|
|
837
|
+
resp = await fetch(`${this.base}/op`, {
|
|
838
|
+
method: "POST",
|
|
839
|
+
headers: { "content-type": "application/json" },
|
|
840
|
+
body: JSON.stringify({ op, args: args ?? {}, progressToken }),
|
|
841
|
+
signal: AbortSignal.timeout(12e4)
|
|
842
|
+
});
|
|
843
|
+
} catch (e) {
|
|
844
|
+
throw new BridgeUnreachableError(this.port, e);
|
|
845
|
+
}
|
|
846
|
+
let data;
|
|
847
|
+
try {
|
|
848
|
+
data = await resp.json();
|
|
849
|
+
} catch {
|
|
850
|
+
throw new AeError(`Bridge returned non-JSON (HTTP ${resp.status})`);
|
|
851
|
+
}
|
|
852
|
+
if (!data.ok) {
|
|
853
|
+
throw new AeError(data.error, data.stack, data.line);
|
|
854
|
+
}
|
|
855
|
+
return data.result;
|
|
856
|
+
}
|
|
857
|
+
async cancel(jobId) {
|
|
858
|
+
try {
|
|
859
|
+
await fetch(`${this.base}/cancel`, {
|
|
860
|
+
method: "POST",
|
|
861
|
+
headers: { "content-type": "application/json" },
|
|
862
|
+
body: JSON.stringify({ jobId })
|
|
863
|
+
});
|
|
864
|
+
} catch (e) {
|
|
865
|
+
logger.warn("cancel failed", e.message);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
// src/bridge/wsClient.ts
|
|
871
|
+
import WebSocket from "ws";
|
|
872
|
+
var WsClient = class {
|
|
873
|
+
constructor(port, jobs) {
|
|
874
|
+
this.port = port;
|
|
875
|
+
this.jobs = jobs;
|
|
876
|
+
}
|
|
877
|
+
port;
|
|
878
|
+
jobs;
|
|
879
|
+
ws;
|
|
880
|
+
reconnectTimer;
|
|
881
|
+
stopped = false;
|
|
882
|
+
start() {
|
|
883
|
+
this.connect();
|
|
884
|
+
}
|
|
885
|
+
stop() {
|
|
886
|
+
this.stopped = true;
|
|
887
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
888
|
+
try {
|
|
889
|
+
this.ws?.close();
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
connect() {
|
|
894
|
+
if (this.stopped) return;
|
|
895
|
+
const url = `ws://127.0.0.1:${this.port}/events`;
|
|
896
|
+
const ws = new WebSocket(url);
|
|
897
|
+
this.ws = ws;
|
|
898
|
+
ws.on("open", () => logger.debug("WS connected", url));
|
|
899
|
+
ws.on("message", (data) => {
|
|
900
|
+
let evt;
|
|
901
|
+
try {
|
|
902
|
+
evt = JSON.parse(data.toString());
|
|
903
|
+
} catch {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
this.handleEvent(evt);
|
|
907
|
+
});
|
|
908
|
+
ws.on("close", () => {
|
|
909
|
+
if (this.stopped) return;
|
|
910
|
+
this.reconnectTimer = setTimeout(() => this.connect(), 1500);
|
|
911
|
+
});
|
|
912
|
+
ws.on("error", () => {
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
handleEvent(evt) {
|
|
916
|
+
switch (evt.type) {
|
|
917
|
+
case "progress":
|
|
918
|
+
this.jobs.reportProgress(evt.jobId, evt.progress, evt.total, evt.message);
|
|
919
|
+
break;
|
|
920
|
+
case "complete":
|
|
921
|
+
this.jobs.complete(evt.jobId, evt.result);
|
|
922
|
+
break;
|
|
923
|
+
case "error":
|
|
924
|
+
this.jobs.fail(evt.jobId, evt.error);
|
|
925
|
+
break;
|
|
926
|
+
case "log":
|
|
927
|
+
logger.debug("[ae]", evt.level, evt.message);
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// src/jobs/manager.ts
|
|
934
|
+
var JobManager = class {
|
|
935
|
+
jobs = /* @__PURE__ */ new Map();
|
|
936
|
+
waiters = /* @__PURE__ */ new Map();
|
|
937
|
+
progressEmitters = /* @__PURE__ */ new Map();
|
|
938
|
+
register(jobId, total) {
|
|
939
|
+
const s = {
|
|
940
|
+
jobId,
|
|
941
|
+
status: "running",
|
|
942
|
+
progress: 0,
|
|
943
|
+
total,
|
|
944
|
+
startedAt: Date.now()
|
|
945
|
+
};
|
|
946
|
+
this.jobs.set(jobId, s);
|
|
947
|
+
return s;
|
|
948
|
+
}
|
|
949
|
+
bindProgressEmitter(jobId, fn) {
|
|
950
|
+
const arr = this.progressEmitters.get(jobId) ?? [];
|
|
951
|
+
arr.push(fn);
|
|
952
|
+
this.progressEmitters.set(jobId, arr);
|
|
953
|
+
}
|
|
954
|
+
reportProgress(jobId, progress, total, message) {
|
|
955
|
+
const s = this.jobs.get(jobId);
|
|
956
|
+
if (s) {
|
|
957
|
+
s.progress = progress;
|
|
958
|
+
if (total !== void 0) s.total = total;
|
|
959
|
+
if (message !== void 0) s.message = message;
|
|
960
|
+
}
|
|
961
|
+
const emitters = this.progressEmitters.get(jobId) ?? [];
|
|
962
|
+
for (const e of emitters) {
|
|
963
|
+
try {
|
|
964
|
+
e(jobId, progress, total, message);
|
|
965
|
+
} catch {
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
complete(jobId, result) {
|
|
970
|
+
const s = this.jobs.get(jobId);
|
|
971
|
+
if (!s) return;
|
|
972
|
+
s.status = "completed";
|
|
973
|
+
s.result = result;
|
|
974
|
+
s.finishedAt = Date.now();
|
|
975
|
+
this.flush(jobId);
|
|
976
|
+
}
|
|
977
|
+
fail(jobId, error) {
|
|
978
|
+
const s = this.jobs.get(jobId);
|
|
979
|
+
if (!s) return;
|
|
980
|
+
s.status = "failed";
|
|
981
|
+
s.error = error;
|
|
982
|
+
s.finishedAt = Date.now();
|
|
983
|
+
this.flush(jobId);
|
|
984
|
+
}
|
|
985
|
+
cancel(jobId) {
|
|
986
|
+
const s = this.jobs.get(jobId);
|
|
987
|
+
if (!s) return;
|
|
988
|
+
s.status = "cancelled";
|
|
989
|
+
s.finishedAt = Date.now();
|
|
990
|
+
this.flush(jobId);
|
|
991
|
+
}
|
|
992
|
+
get(jobId) {
|
|
993
|
+
return this.jobs.get(jobId);
|
|
994
|
+
}
|
|
995
|
+
async waitFor(jobId, timeoutMs = 6e5) {
|
|
996
|
+
const cur = this.jobs.get(jobId);
|
|
997
|
+
if (cur && (cur.status === "completed" || cur.status === "failed" || cur.status === "cancelled")) {
|
|
998
|
+
if (cur.status === "failed") throw new Error(cur.error || "job failed");
|
|
999
|
+
return cur;
|
|
1000
|
+
}
|
|
1001
|
+
return new Promise((resolve, reject) => {
|
|
1002
|
+
const arr = this.waiters.get(jobId) ?? [];
|
|
1003
|
+
arr.push({ resolve, reject });
|
|
1004
|
+
this.waiters.set(jobId, arr);
|
|
1005
|
+
const t = setTimeout(() => reject(new Error(`await_job(${jobId}) timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
1006
|
+
const orig = arr[arr.length - 1];
|
|
1007
|
+
arr[arr.length - 1] = {
|
|
1008
|
+
resolve: (s) => {
|
|
1009
|
+
clearTimeout(t);
|
|
1010
|
+
orig.resolve(s);
|
|
1011
|
+
},
|
|
1012
|
+
reject: (e) => {
|
|
1013
|
+
clearTimeout(t);
|
|
1014
|
+
orig.reject(e);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
flush(jobId) {
|
|
1020
|
+
const s = this.jobs.get(jobId);
|
|
1021
|
+
if (!s) return;
|
|
1022
|
+
const waiters = this.waiters.get(jobId) ?? [];
|
|
1023
|
+
this.waiters.delete(jobId);
|
|
1024
|
+
for (const w of waiters) {
|
|
1025
|
+
if (s.status === "failed") w.reject(new Error(s.error || "job failed"));
|
|
1026
|
+
else w.resolve(s);
|
|
1027
|
+
}
|
|
1028
|
+
this.progressEmitters.delete(jobId);
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
// src/tools/descriptions.ts
|
|
1033
|
+
var descriptions = {
|
|
1034
|
+
// ---------- comps ----------
|
|
1035
|
+
list_comps: "All comps: id, name, dims, duration, fps, layer count.",
|
|
1036
|
+
get_comp: "Single comp summary by id (no layers).",
|
|
1037
|
+
get_comp_tree: "Comp + nested layer tree, recursing pre-comps to `depth`.",
|
|
1038
|
+
create_comp: "Create a new comp. Returns id.",
|
|
1039
|
+
set_comp: "Modify a comp (name, dims, fps, duration, work area, bg). Undefined fields unchanged.",
|
|
1040
|
+
delete_comp: "Delete a comp. Reversible only via AE's Undo.",
|
|
1041
|
+
set_active_comp: "Focus a comp in the viewer/timeline.",
|
|
1042
|
+
// ---------- layers ----------
|
|
1043
|
+
list_layers: "Layers in a comp, one-line each. Use get_layer_full for details.",
|
|
1044
|
+
get_layer_full: "Full state of one layer: transform + keyframes + expressions, effects, masks, markers, parenting, text/shape/footage extras, and sourceRect (visible bounds). Always prefer over multiple smaller queries.",
|
|
1045
|
+
create_text_layer: "Text layer with optional font/size/color/position. anchorAlign defaults to 'left' so position means the visible left edge.",
|
|
1046
|
+
create_shape_layer: "Empty shape layer; fill via add_shape_content.",
|
|
1047
|
+
create_solid_layer: "Solid-color layer. color is RGB 0..1.",
|
|
1048
|
+
create_null_layer: "Null parent layer.",
|
|
1049
|
+
create_adjustment_layer: "Adjustment layer \u2014 effects on it apply to layers below.",
|
|
1050
|
+
create_precomp_layer: "Insert an existing comp into another as a pre-comp layer.",
|
|
1051
|
+
create_camera_layer: "Camera. 3D layers below view through it.",
|
|
1052
|
+
create_light_layer: "Light (parallel|spot|point|ambient).",
|
|
1053
|
+
duplicate_layer: "Duplicate a layer N times; each has a fresh id.",
|
|
1054
|
+
delete_layer: "Remove a layer.",
|
|
1055
|
+
set_layer: "Update layer metadata (name/enabled/locked/shy/solo/3D/blend/label/in-out/stretch/trackMatte). Undefined fields unchanged.",
|
|
1056
|
+
parent_layer: "Set/clear a layer's parent (parentLayerId=null to unparent).",
|
|
1057
|
+
reorder_layer: "Move layer to 1-based stack index.",
|
|
1058
|
+
// ---------- transforms ----------
|
|
1059
|
+
set_transform: "Set any of position/scale/rotation/anchorPoint/opacity (+3D orientation/per-axis on 3D). keyframe:true + time sets keyframes.",
|
|
1060
|
+
// ---------- keyframes ----------
|
|
1061
|
+
add_keyframe: "Keyframe at `time` on propertyPath (e.g. ['Transform','Position']). Optional in/out interpolation + ease.",
|
|
1062
|
+
remove_keyframe: "Remove keyframe at `time`.",
|
|
1063
|
+
get_keyframes: "All keyframes on a property: time, value, interpolation, ease, spatial tangents.",
|
|
1064
|
+
set_interpolation: "Set in/out interpolation type of a specific keyframe.",
|
|
1065
|
+
set_temporal_ease: "Set influence+speed for in/out ease of a keyframe.",
|
|
1066
|
+
set_spatial_tangents: "Set in/out spatial tangents for a position-style keyframe.",
|
|
1067
|
+
// ---------- expressions ----------
|
|
1068
|
+
get_expression: "Expression text + enabled state on a property.",
|
|
1069
|
+
set_expression: "Set + enable an expression on a property.",
|
|
1070
|
+
toggle_expression: "Enable/disable an expression without clearing.",
|
|
1071
|
+
clear_expression: "Remove an expression.",
|
|
1072
|
+
// ---------- effects ----------
|
|
1073
|
+
list_effects: "All effects on a layer with current param values + keyframes/expressions.",
|
|
1074
|
+
add_effect: "Add effect by matchName (use list_available_effects \u2014 matchNames are stable across AE versions; display names aren't).",
|
|
1075
|
+
remove_effect: "Remove effect by 1-based index.",
|
|
1076
|
+
set_effect_param: "Set an effect param by name/matchName. keyframe:true+time for keyframed value.",
|
|
1077
|
+
set_effect_enabled: "Toggle an effect on/off without removing.",
|
|
1078
|
+
list_available_effects: "All effects installed in this AE: displayName, matchName, category.",
|
|
1079
|
+
// ---------- text ----------
|
|
1080
|
+
set_text: "Set text content + styling (font, size, fill/stroke, tracking, leading, justification). Undefined fields unchanged.",
|
|
1081
|
+
add_text_animator: "Add a text animator group (position/scale/rotation/opacity/tracking/skew/fillColor/strokeColor). Optional range selector.",
|
|
1082
|
+
// ---------- shapes ----------
|
|
1083
|
+
set_shape_path: "Replace a shape path with new vertices/tangents/closed.",
|
|
1084
|
+
add_shape_content: "Add shape content under Contents or a sub-group. `content.type` picks the node (rect/ellipse/star/path/fill/stroke/trim/repeater/merge/group); the other keys set its properties by friendly name (e.g. rect: size/position/roundness; fill: color/opacity; stroke: color/width/lineCap; path: vertices/inTangents/outTangents/closed). Unknown or unsettable keys are NOT ignored \u2014 the whole node is rolled back and an error lists them, so trust a success result. Use get_layer_full to see exact property names.",
|
|
1085
|
+
set_shape_property: "Set a property on a shape content node. Optional keyframe at time.",
|
|
1086
|
+
// ---------- masks ----------
|
|
1087
|
+
add_mask: "Add a mask with vertices/tangents/closed/mode.",
|
|
1088
|
+
set_mask: "Modify mask vertices/tangents/closed/mode/inverted/expansion/feather/opacity.",
|
|
1089
|
+
remove_mask: "Remove a mask by 1-based index.",
|
|
1090
|
+
// ---------- markers ----------
|
|
1091
|
+
add_marker: "Add a marker on a layer (layerId) or on the comp. time + optional duration/comment/label/chapter/url/frameTarget.",
|
|
1092
|
+
remove_marker: "Remove a marker by 1-based index.",
|
|
1093
|
+
// ---------- vision ----------
|
|
1094
|
+
screenshot_frame: "ONE-OFF visual check of a comp at a time. Base64 PNG. Use only at key moments \u2014 never per-frame or in a loop. For motion, 2-3 snapshots + get_layer_full property values. Set downsample:2 (or 3-4 on 4K comps) to keep the image small \u2014 full-res 4K frames are large enough to exhaust the context. The result reports the dimensions actually returned, and warns if a requested downsample could not be applied.",
|
|
1095
|
+
screenshot_layer: "ONE-OFF visual check of a single layer (solo'd) at a time. Same one-off rule and same downsample guidance as screenshot_frame.",
|
|
1096
|
+
// ---------- batch ----------
|
|
1097
|
+
run_batch: "Many ops in one ExtendScript pass, one undo step. >500 ops returns a jobId + streams progress; use await_job. transactional:true (default) rolls back on first error.",
|
|
1098
|
+
// ---------- explore ----------
|
|
1099
|
+
get_project_summary: "Project state: path, item count, active item, flat item list with type.",
|
|
1100
|
+
find_layers: "Search across one or all comps for layers matching name/type/effect filters.",
|
|
1101
|
+
// ---------- raw ----------
|
|
1102
|
+
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.",
|
|
1103
|
+
// ---------- jobs ----------
|
|
1104
|
+
await_job: "Block until job is done. Default 10min timeout. Returns the same payload the tool would have.",
|
|
1105
|
+
get_job: "Non-blocking job status: progress/total/state/error.",
|
|
1106
|
+
cancel_job: "Set cancel flag; chunked loop stops at next boundary.",
|
|
1107
|
+
// ---------- 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 or out of date. It writes to the user's Adobe CEP extensions folder and sets a user-level Adobe preference \u2014 tell the user what it will do before calling it. Afterwards, AE must be restarted; if the preference was newly enabled, a one-time Mac reboot may also be needed."
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// src/setup/check.ts
|
|
1113
|
+
import { execFile } from "node:child_process";
|
|
1114
|
+
import crypto from "node:crypto";
|
|
1115
|
+
import fs4 from "node:fs";
|
|
1116
|
+
import path4 from "node:path";
|
|
1117
|
+
import { promisify } from "node:util";
|
|
1118
|
+
|
|
1119
|
+
// src/setup/paths.ts
|
|
1120
|
+
import fs3 from "node:fs";
|
|
1121
|
+
import os2 from "node:os";
|
|
1122
|
+
import path3 from "node:path";
|
|
1123
|
+
import { createRequire } from "node:module";
|
|
1124
|
+
import { fileURLToPath } from "node:url";
|
|
1125
|
+
var BUNDLE_ID = "games.engine-room.ae-mcp";
|
|
1126
|
+
function packageRoot() {
|
|
1127
|
+
let dir = path3.dirname(fileURLToPath(import.meta.url));
|
|
1128
|
+
for (let i = 0; i < 8; i++) {
|
|
1129
|
+
if (fs3.existsSync(path3.join(dir, "package.json"))) return dir;
|
|
1130
|
+
const parent = path3.dirname(dir);
|
|
1131
|
+
if (parent === dir) break;
|
|
1132
|
+
dir = parent;
|
|
1133
|
+
}
|
|
1134
|
+
return path3.dirname(fileURLToPath(import.meta.url));
|
|
1135
|
+
}
|
|
1136
|
+
function panelSourceDir() {
|
|
1137
|
+
const candidates = [
|
|
1138
|
+
// The live workspace copy comes first so a git checkout always installs
|
|
1139
|
+
// what the developer is editing, never a stale vendored copy left behind by
|
|
1140
|
+
// a previous `npm pack`. Only the second path exists in the tarball.
|
|
1141
|
+
path3.resolve(packageRoot(), "..", "ae-panel"),
|
|
1142
|
+
path3.join(packageRoot(), "panel")
|
|
1143
|
+
];
|
|
1144
|
+
for (const dir of candidates) {
|
|
1145
|
+
if (fs3.existsSync(path3.join(dir, "CSXS", "manifest.xml"))) return dir;
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
function cepExtensionsDir() {
|
|
1150
|
+
return path3.join(os2.homedir(), "Library", "Application Support", "Adobe", "CEP", "extensions");
|
|
1151
|
+
}
|
|
1152
|
+
function installedPanelDir() {
|
|
1153
|
+
return path3.join(cepExtensionsDir(), BUNDLE_ID);
|
|
1154
|
+
}
|
|
1155
|
+
function wsModuleDir() {
|
|
1156
|
+
try {
|
|
1157
|
+
const require2 = createRequire(import.meta.url);
|
|
1158
|
+
const entry = require2.resolve("ws");
|
|
1159
|
+
const marker = `${path3.sep}node_modules${path3.sep}ws${path3.sep}`;
|
|
1160
|
+
const idx = entry.lastIndexOf(marker);
|
|
1161
|
+
if (idx >= 0) return entry.slice(0, idx + marker.length - 1);
|
|
1162
|
+
return path3.dirname(entry);
|
|
1163
|
+
} catch {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function copyRecursive(src, dst) {
|
|
1168
|
+
const stat = fs3.lstatSync(src);
|
|
1169
|
+
if (stat.isDirectory()) {
|
|
1170
|
+
fs3.mkdirSync(dst, { recursive: true });
|
|
1171
|
+
for (const entry of fs3.readdirSync(src)) {
|
|
1172
|
+
copyRecursive(path3.join(src, entry), path3.join(dst, entry));
|
|
1173
|
+
}
|
|
1174
|
+
} else if (stat.isSymbolicLink()) {
|
|
1175
|
+
fs3.symlinkSync(fs3.readlinkSync(src), dst);
|
|
1176
|
+
} else {
|
|
1177
|
+
fs3.copyFileSync(src, dst);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/setup/check.ts
|
|
1182
|
+
var exec = promisify(execFile);
|
|
1183
|
+
var CSXS_VERSIONS = [12, 11, 10, 9];
|
|
1184
|
+
async function playerDebugMode() {
|
|
1185
|
+
for (const v of CSXS_VERSIONS) {
|
|
1186
|
+
try {
|
|
1187
|
+
const { stdout } = await exec("defaults", ["read", `com.adobe.CSXS.${v}`, "PlayerDebugMode"]);
|
|
1188
|
+
if (stdout.trim() === "1") return { on: true, detail: `enabled (CSXS.${v})` };
|
|
1189
|
+
} catch {
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return { on: false, detail: "not enabled for any CSXS version" };
|
|
1193
|
+
}
|
|
1194
|
+
async function aeRunning() {
|
|
1195
|
+
try {
|
|
1196
|
+
const { stdout } = await exec("pgrep", ["-f", "Adobe After Effects"]);
|
|
1197
|
+
return stdout.trim().length > 0;
|
|
1198
|
+
} catch {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function sha256(file) {
|
|
1203
|
+
try {
|
|
1204
|
+
return crypto.createHash("sha256").update(fs4.readFileSync(file)).digest("hex");
|
|
1205
|
+
} catch {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
async function bridgeReachable(port) {
|
|
1210
|
+
try {
|
|
1211
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2e3) });
|
|
1212
|
+
if (!res.ok) return { ok: false, detail: `port ${port} returned HTTP ${res.status}` };
|
|
1213
|
+
return { ok: true, detail: `responding on port ${port}` };
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
return { ok: false, detail: `no response on port ${port} (${e.message})` };
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
async function checkSetup() {
|
|
1219
|
+
const checks = [];
|
|
1220
|
+
const isMac = process.platform === "darwin";
|
|
1221
|
+
checks.push({
|
|
1222
|
+
name: "platform",
|
|
1223
|
+
ok: isMac,
|
|
1224
|
+
detail: process.platform,
|
|
1225
|
+
fix: isMac ? void 0 : "This server currently supports macOS only. Windows needs a different CEP install path and a registry edit."
|
|
1226
|
+
});
|
|
1227
|
+
const source = panelSourceDir();
|
|
1228
|
+
checks.push({
|
|
1229
|
+
name: "panelAssetsPresent",
|
|
1230
|
+
ok: source !== null,
|
|
1231
|
+
detail: source ?? "not found in this installation",
|
|
1232
|
+
fix: source ? void 0 : "The package is missing its CEP panel assets \u2014 reinstall the server."
|
|
1233
|
+
});
|
|
1234
|
+
const debugMode = await playerDebugMode();
|
|
1235
|
+
checks.push({
|
|
1236
|
+
name: "cepDebugMode",
|
|
1237
|
+
ok: debugMode.on,
|
|
1238
|
+
detail: debugMode.detail,
|
|
1239
|
+
fix: debugMode.on ? void 0 : "Run the setup_panel tool. After Effects only loads unsigned panels when this Adobe preference is set."
|
|
1240
|
+
});
|
|
1241
|
+
const installed = installedPanelDir();
|
|
1242
|
+
const isInstalled = fs4.existsSync(path4.join(installed, "CSXS", "manifest.xml"));
|
|
1243
|
+
checks.push({
|
|
1244
|
+
name: "panelInstalled",
|
|
1245
|
+
ok: isInstalled,
|
|
1246
|
+
detail: isInstalled ? installed : `not present at ${installed}`,
|
|
1247
|
+
fix: isInstalled ? void 0 : "Run the setup_panel tool to install it."
|
|
1248
|
+
});
|
|
1249
|
+
if (isInstalled && source) {
|
|
1250
|
+
const installedHash = sha256(path4.join(installed, "jsx", "bundle.jsx"));
|
|
1251
|
+
const sourceHash = sha256(path4.join(source, "jsx", "bundle.jsx"));
|
|
1252
|
+
const upToDate = installedHash !== null && installedHash === sourceHash;
|
|
1253
|
+
checks.push({
|
|
1254
|
+
name: "panelUpToDate",
|
|
1255
|
+
ok: upToDate,
|
|
1256
|
+
detail: upToDate ? "installed panel matches this server version" : "installed panel differs from the version shipped with this server",
|
|
1257
|
+
fix: upToDate ? void 0 : "Run setup_panel to refresh it, then restart After Effects."
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
const running = await aeRunning();
|
|
1261
|
+
checks.push({
|
|
1262
|
+
name: "afterEffectsRunning",
|
|
1263
|
+
ok: running,
|
|
1264
|
+
detail: running ? "running" : "not running",
|
|
1265
|
+
fix: running ? void 0 : "Launch After Effects. The panel starts automatically with it."
|
|
1266
|
+
});
|
|
1267
|
+
const port = discoverPort();
|
|
1268
|
+
const bridge = await bridgeReachable(port);
|
|
1269
|
+
checks.push({
|
|
1270
|
+
name: "bridgeReachable",
|
|
1271
|
+
ok: bridge.ok,
|
|
1272
|
+
detail: bridge.detail,
|
|
1273
|
+
fix: bridge.ok ? void 0 : "If the other checks pass, restart After Effects so the panel reloads."
|
|
1274
|
+
});
|
|
1275
|
+
const ready = checks.every((c) => c.ok);
|
|
1276
|
+
return { ready, checks, nextSteps: buildNextSteps(checks, ready) };
|
|
1277
|
+
}
|
|
1278
|
+
function buildNextSteps(checks, ready) {
|
|
1279
|
+
if (ready) return ["Everything is connected. After Effects is ready to drive."];
|
|
1280
|
+
const by = (name) => checks.find((c) => c.name === name);
|
|
1281
|
+
const steps = [];
|
|
1282
|
+
if (by("platform")?.ok === false) return [by("platform").fix];
|
|
1283
|
+
if (by("panelAssetsPresent")?.ok === false) return [by("panelAssetsPresent").fix];
|
|
1284
|
+
const needsInstall = by("panelInstalled")?.ok === false || by("panelUpToDate")?.ok === false;
|
|
1285
|
+
const needsDebug = by("cepDebugMode")?.ok === false;
|
|
1286
|
+
if (needsDebug || needsInstall) {
|
|
1287
|
+
steps.push("Run the setup_panel tool \u2014 it installs the After Effects panel and enables the Adobe preference that lets AE load it.");
|
|
1288
|
+
}
|
|
1289
|
+
if (needsDebug) {
|
|
1290
|
+
steps.push("Quit and reopen After Effects. If the panel still does not connect, restart the Mac once \u2014 the Adobe preference sometimes only takes effect after a reboot.");
|
|
1291
|
+
}
|
|
1292
|
+
if (by("afterEffectsRunning")?.ok === false) {
|
|
1293
|
+
steps.push("Open After Effects 2026.");
|
|
1294
|
+
} else if (needsInstall) {
|
|
1295
|
+
steps.push("Quit and reopen After Effects so it picks up the panel.");
|
|
1296
|
+
}
|
|
1297
|
+
if (steps.length === 0 && by("bridgeReachable")?.ok === false) {
|
|
1298
|
+
steps.push("Everything is installed but the panel is not answering. Quit and reopen After Effects.");
|
|
1299
|
+
steps.push("If it still fails, open Window > Extensions > AE MCP Bridge in AE to see the panel's own status log.");
|
|
1300
|
+
}
|
|
1301
|
+
return steps;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/setup/install.ts
|
|
1305
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1306
|
+
import fs5 from "node:fs";
|
|
1307
|
+
import path5 from "node:path";
|
|
1308
|
+
import { promisify as promisify2 } from "node:util";
|
|
1309
|
+
var exec2 = promisify2(execFile2);
|
|
1310
|
+
var CSXS_VERSIONS2 = [12, 11, 10, 9];
|
|
1311
|
+
async function isDebugModeOn() {
|
|
1312
|
+
for (const v of CSXS_VERSIONS2) {
|
|
1313
|
+
try {
|
|
1314
|
+
const { stdout } = await exec2("defaults", ["read", `com.adobe.CSXS.${v}`, "PlayerDebugMode"]);
|
|
1315
|
+
if (stdout.trim() === "1") return true;
|
|
1316
|
+
} catch {
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
async function enableDebugMode() {
|
|
1322
|
+
const enabled = [];
|
|
1323
|
+
for (const v of CSXS_VERSIONS2) {
|
|
1324
|
+
try {
|
|
1325
|
+
await exec2("defaults", ["write", `com.adobe.CSXS.${v}`, "PlayerDebugMode", "1"]);
|
|
1326
|
+
enabled.push(v);
|
|
1327
|
+
} catch {
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return enabled;
|
|
1331
|
+
}
|
|
1332
|
+
async function installPanel(opts = {}) {
|
|
1333
|
+
const actions = [];
|
|
1334
|
+
const notes = [];
|
|
1335
|
+
if (process.platform !== "darwin") {
|
|
1336
|
+
throw new Error("setup_panel supports macOS only. On Windows the CEP extensions folder and the PlayerDebugMode registry key differ; install manually.");
|
|
1337
|
+
}
|
|
1338
|
+
const source = panelSourceDir();
|
|
1339
|
+
if (!source) {
|
|
1340
|
+
throw new Error("Could not find the CEP panel assets that ship with this server. Reinstall the package.");
|
|
1341
|
+
}
|
|
1342
|
+
if (!fs5.existsSync(path5.join(source, "jsx", "bundle.jsx"))) {
|
|
1343
|
+
throw new Error(`The panel at ${source} has no jsx/bundle.jsx. In a git checkout, run \`npm run build:jsx\` first.`);
|
|
1344
|
+
}
|
|
1345
|
+
const target = installedPanelDir();
|
|
1346
|
+
const existing = fs5.lstatSync(target, { throwIfNoEntry: false });
|
|
1347
|
+
if (existing?.isSymbolicLink() && !opts.force) {
|
|
1348
|
+
const linkTarget = fs5.readlinkSync(target);
|
|
1349
|
+
return {
|
|
1350
|
+
ok: true,
|
|
1351
|
+
panelPath: target,
|
|
1352
|
+
actions: [],
|
|
1353
|
+
restartAfterEffects: false,
|
|
1354
|
+
rebootRecommended: false,
|
|
1355
|
+
notes: [
|
|
1356
|
+
`Left the existing symlinked panel alone (it points at ${linkTarget}).`,
|
|
1357
|
+
"That is a development install, so it already tracks its source. Pass force:true to replace it with a copy."
|
|
1358
|
+
]
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
if (existing) {
|
|
1362
|
+
fs5.rmSync(target, { recursive: true, force: true });
|
|
1363
|
+
actions.push("Removed the previously installed panel.");
|
|
1364
|
+
}
|
|
1365
|
+
fs5.mkdirSync(path5.dirname(target), { recursive: true });
|
|
1366
|
+
copyRecursive(source, target);
|
|
1367
|
+
actions.push(`Installed the panel to ${target}.`);
|
|
1368
|
+
const ws = wsModuleDir();
|
|
1369
|
+
if (ws) {
|
|
1370
|
+
const dest = path5.join(target, "node_modules", "ws");
|
|
1371
|
+
fs5.mkdirSync(path5.dirname(dest), { recursive: true });
|
|
1372
|
+
fs5.rmSync(dest, { recursive: true, force: true });
|
|
1373
|
+
copyRecursive(ws, dest);
|
|
1374
|
+
actions.push("Copied the `ws` module the panel needs at runtime.");
|
|
1375
|
+
} else {
|
|
1376
|
+
notes.push("Could not locate the `ws` module \u2014 the panel's WebSocket events may not work. Reinstall the package if progress notifications fail.");
|
|
1377
|
+
}
|
|
1378
|
+
let rebootRecommended = false;
|
|
1379
|
+
if (opts.enableDebugMode !== false) {
|
|
1380
|
+
const wasOn = await isDebugModeOn();
|
|
1381
|
+
if (wasOn) {
|
|
1382
|
+
actions.push("Adobe's PlayerDebugMode preference was already enabled.");
|
|
1383
|
+
} else {
|
|
1384
|
+
const versions = await enableDebugMode();
|
|
1385
|
+
if (versions.length === 0) {
|
|
1386
|
+
notes.push("Failed to set PlayerDebugMode. Without it After Effects will refuse to load this panel.");
|
|
1387
|
+
} else {
|
|
1388
|
+
actions.push(`Enabled PlayerDebugMode for CSXS ${versions.join(", ")} so AE will load the unsigned panel.`);
|
|
1389
|
+
rebootRecommended = true;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (rebootRecommended) {
|
|
1394
|
+
notes.push("PlayerDebugMode was just turned on. Quit and reopen After Effects; if the panel still does not connect, restart the Mac once \u2014 on some macOS builds the preference only applies after a reboot.");
|
|
1395
|
+
}
|
|
1396
|
+
return {
|
|
1397
|
+
ok: true,
|
|
1398
|
+
panelPath: target,
|
|
1399
|
+
actions,
|
|
1400
|
+
restartAfterEffects: true,
|
|
1401
|
+
rebootRecommended,
|
|
1402
|
+
notes
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/util/pngImage.ts
|
|
1407
|
+
function imageContent(meta, base64) {
|
|
1408
|
+
return {
|
|
1409
|
+
content: [
|
|
1410
|
+
{ type: "text", text: JSON.stringify(meta) },
|
|
1411
|
+
{ type: "image", data: base64, mimeType: "image/png" }
|
|
1412
|
+
]
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/server.ts
|
|
1417
|
+
var { OpSchemas: OpSchemas2 } = schemas_exports;
|
|
1418
|
+
var VISION_OPS = /* @__PURE__ */ new Set(["screenshot_frame", "screenshot_layer"]);
|
|
1419
|
+
var ASYNC_OPS = /* @__PURE__ */ new Set(["run_batch"]);
|
|
1420
|
+
var SERVER_OPS = /* @__PURE__ */ new Set(["await_job", "get_job", "cancel_job", "check_setup", "setup_panel"]);
|
|
1421
|
+
var AwaitJobSchema = schemas_exports.AwaitJob;
|
|
1422
|
+
var GetJobSchema = schemas_exports.GetJob;
|
|
1423
|
+
var CancelJobSchema = schemas_exports.CancelJob;
|
|
1424
|
+
function createServer() {
|
|
1425
|
+
const server = new Server(
|
|
1426
|
+
{ name: "after-effects-mcp", version: "0.1.0" },
|
|
1427
|
+
{ capabilities: { tools: {}, logging: {} } }
|
|
1428
|
+
);
|
|
1429
|
+
const bridge = new HttpClient();
|
|
1430
|
+
const jobs = new JobManager();
|
|
1431
|
+
const ws = new WsClient(bridge.port, jobs);
|
|
1432
|
+
ws.start();
|
|
1433
|
+
bridge.health().then(
|
|
1434
|
+
(h) => logger.info(`Bridge healthy on port ${h.port}`),
|
|
1435
|
+
(e) => logger.warn(`Bridge not reachable yet: ${e.message}`)
|
|
1436
|
+
);
|
|
1437
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1438
|
+
const tools = Object.keys(OpSchemas2).map((name) => {
|
|
1439
|
+
const schema = OpSchemas2[name];
|
|
1440
|
+
const jsonSchema = zodToJsonSchema(schema, {
|
|
1441
|
+
target: "jsonSchema7",
|
|
1442
|
+
$refStrategy: "none"
|
|
1443
|
+
});
|
|
1444
|
+
return {
|
|
1445
|
+
name,
|
|
1446
|
+
description: descriptions[name] ?? `AE op: ${name}`,
|
|
1447
|
+
inputSchema: toDraft2020(jsonSchema)
|
|
1448
|
+
};
|
|
1449
|
+
});
|
|
1450
|
+
return { tools };
|
|
1451
|
+
});
|
|
1452
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
1453
|
+
const name = req.params.name;
|
|
1454
|
+
const rawArgs = req.params.arguments ?? {};
|
|
1455
|
+
const progressToken = req.params?._meta?.progressToken;
|
|
1456
|
+
if (!(name in OpSchemas2) && !SERVER_OPS.has(name)) {
|
|
1457
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
1458
|
+
}
|
|
1459
|
+
if (SERVER_OPS.has(name)) {
|
|
1460
|
+
try {
|
|
1461
|
+
if (name === "await_job") {
|
|
1462
|
+
const a = AwaitJobSchema.parse(rawArgs);
|
|
1463
|
+
const st = await jobs.waitFor(a.jobId, a.timeoutMs ?? 6e5);
|
|
1464
|
+
return textResult(st);
|
|
1465
|
+
}
|
|
1466
|
+
if (name === "get_job") {
|
|
1467
|
+
const a = GetJobSchema.parse(rawArgs);
|
|
1468
|
+
const st = jobs.get(a.jobId);
|
|
1469
|
+
return textResult(st ?? null);
|
|
1470
|
+
}
|
|
1471
|
+
if (name === "cancel_job") {
|
|
1472
|
+
const a = CancelJobSchema.parse(rawArgs);
|
|
1473
|
+
await bridge.cancel(a.jobId);
|
|
1474
|
+
jobs.cancel(a.jobId);
|
|
1475
|
+
return textResult({ ok: true });
|
|
1476
|
+
}
|
|
1477
|
+
if (name === "check_setup") {
|
|
1478
|
+
return textResult(await checkSetup());
|
|
1479
|
+
}
|
|
1480
|
+
if (name === "setup_panel") {
|
|
1481
|
+
const a = schemas_exports.SetupPanel.parse(rawArgs);
|
|
1482
|
+
const installed = await installPanel({ enableDebugMode: a.enableDebugMode, force: a.force });
|
|
1483
|
+
return textResult({ ...installed, setup: await checkSetup() });
|
|
1484
|
+
}
|
|
1485
|
+
} catch (e) {
|
|
1486
|
+
return errorResult(e.message);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
let args;
|
|
1490
|
+
try {
|
|
1491
|
+
args = OpSchemas2[name].parse(rawArgs);
|
|
1492
|
+
} catch (e) {
|
|
1493
|
+
return errorResult(`Invalid arguments for ${name}: ${e.message}`);
|
|
1494
|
+
}
|
|
1495
|
+
try {
|
|
1496
|
+
const result = await bridge.runOp(name, args, progressToken);
|
|
1497
|
+
if (ASYNC_OPS.has(name) && isAsyncEnvelope(result)) {
|
|
1498
|
+
const env = result;
|
|
1499
|
+
jobs.register(env.jobId, env.total);
|
|
1500
|
+
if (progressToken !== void 0) {
|
|
1501
|
+
jobs.bindProgressEmitter(env.jobId, (jid, progress, total, message) => {
|
|
1502
|
+
void server.notification({
|
|
1503
|
+
method: "notifications/progress",
|
|
1504
|
+
params: { progressToken, progress, total, message }
|
|
1505
|
+
});
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
return textResult({ jobId: env.jobId, async: true, total: env.total });
|
|
1509
|
+
}
|
|
1510
|
+
if (VISION_OPS.has(name) && isVisionResult(result)) {
|
|
1511
|
+
const v = result;
|
|
1512
|
+
return imageContent(
|
|
1513
|
+
{
|
|
1514
|
+
width: v.width,
|
|
1515
|
+
height: v.height,
|
|
1516
|
+
fullWidth: v.fullWidth,
|
|
1517
|
+
fullHeight: v.fullHeight,
|
|
1518
|
+
downsample: v.downsample,
|
|
1519
|
+
time: v.time,
|
|
1520
|
+
compId: v.compId,
|
|
1521
|
+
layerId: v.layerId,
|
|
1522
|
+
bytes: v.bytes,
|
|
1523
|
+
// Surfaced when a requested downsample could not be applied, so the
|
|
1524
|
+
// agent knows it is looking at a full-resolution frame.
|
|
1525
|
+
warning: v.warning
|
|
1526
|
+
},
|
|
1527
|
+
v.base64
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
return textResult(result);
|
|
1531
|
+
} catch (e) {
|
|
1532
|
+
if (e instanceof BridgeUnreachableError) return errorResult(e.message);
|
|
1533
|
+
if (e instanceof AeError) return errorResult(`AE: ${e.message}${e.line ? ` (line ${e.line})` : ""}`);
|
|
1534
|
+
return errorResult(e.message);
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
return server;
|
|
1538
|
+
}
|
|
1539
|
+
function textResult(value) {
|
|
1540
|
+
return {
|
|
1541
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
function errorResult(message) {
|
|
1545
|
+
return {
|
|
1546
|
+
content: [{ type: "text", text: message }],
|
|
1547
|
+
isError: true
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function toDraft2020(node) {
|
|
1551
|
+
if (Array.isArray(node)) return node.map(toDraft2020);
|
|
1552
|
+
if (!node || typeof node !== "object") return node;
|
|
1553
|
+
const out = {};
|
|
1554
|
+
for (const k of Object.keys(node)) out[k] = toDraft2020(node[k]);
|
|
1555
|
+
if (out.type === "array" && Array.isArray(out.items)) {
|
|
1556
|
+
out.prefixItems = out.items;
|
|
1557
|
+
if ("additionalItems" in out) {
|
|
1558
|
+
out.items = out.additionalItems;
|
|
1559
|
+
delete out.additionalItems;
|
|
1560
|
+
} else {
|
|
1561
|
+
out.items = false;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return out;
|
|
1565
|
+
}
|
|
1566
|
+
function isAsyncEnvelope(v) {
|
|
1567
|
+
return !!(v && typeof v === "object" && "async" in v && v.async === true && "jobId" in v);
|
|
1568
|
+
}
|
|
1569
|
+
function isVisionResult(v) {
|
|
1570
|
+
return !!(v && typeof v === "object" && "base64" in v && "width" in v);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/index.ts
|
|
1574
|
+
var USAGE = `@engine-room/after-effects-mcp \u2014 drive Adobe After Effects from an AI agent.
|
|
1575
|
+
|
|
1576
|
+
With no arguments it runs as an MCP server over stdio, which is how an MCP
|
|
1577
|
+
client (Claude Code, Claude Desktop, \u2026) starts it. You do not normally run it
|
|
1578
|
+
by hand.
|
|
1579
|
+
|
|
1580
|
+
Commands:
|
|
1581
|
+
init <directory> [--with-mcp] Create a project folder with a house-style
|
|
1582
|
+
skill you can edit. Pass --with-mcp if you
|
|
1583
|
+
did not install the Claude Code plugin.
|
|
1584
|
+
--help Show this message.
|
|
1585
|
+
--version Print the version.
|
|
1586
|
+
`;
|
|
1587
|
+
async function main() {
|
|
1588
|
+
const argv = process.argv.slice(2);
|
|
1589
|
+
if (argv[0] === "init") {
|
|
1590
|
+
process.exit(runInit(argv.slice(1)));
|
|
1591
|
+
}
|
|
1592
|
+
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
1593
|
+
process.stdout.write(USAGE);
|
|
1594
|
+
process.exit(0);
|
|
1595
|
+
}
|
|
1596
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
1597
|
+
process.stdout.write(`${VERSION}
|
|
1598
|
+
`);
|
|
1599
|
+
process.exit(0);
|
|
1600
|
+
}
|
|
1601
|
+
if (argv.length > 0) {
|
|
1602
|
+
process.stderr.write(`Unknown command: ${argv[0]}
|
|
1603
|
+
|
|
1604
|
+
${USAGE}`);
|
|
1605
|
+
process.exit(1);
|
|
1606
|
+
}
|
|
1607
|
+
const server = createServer();
|
|
1608
|
+
const transport = new StdioServerTransport();
|
|
1609
|
+
await server.connect(transport);
|
|
1610
|
+
logger.info("MCP server running on stdio");
|
|
1611
|
+
}
|
|
1612
|
+
var VERSION = "0.1.0";
|
|
1613
|
+
main().catch((e) => {
|
|
1614
|
+
logger.error("fatal", e.message);
|
|
1615
|
+
process.exit(1);
|
|
1616
|
+
});
|