@engine-room/after-effects-mcp 0.2.1 → 0.3.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 +20 -1
- package/bin/server.js +275 -53
- package/package.json +1 -1
- package/panel/CSXS/manifest.xml +2 -2
- package/panel/client/framecache.js +98 -0
- package/panel/client/main.js +240 -15
- package/panel/client/mogrt.js +328 -0
- package/panel/client/pngcodec.js +402 -0
- package/panel/jsx/bundle.jsx +1187 -157
- package/panel/package.json +1 -1
package/bin/server.js
CHANGED
|
@@ -352,6 +352,7 @@ __export(schemas_exports, {
|
|
|
352
352
|
CreateAdjustmentLayer: () => CreateAdjustmentLayer,
|
|
353
353
|
CreateCameraLayer: () => CreateCameraLayer,
|
|
354
354
|
CreateComp: () => CreateComp,
|
|
355
|
+
CreateFootageLayer: () => CreateFootageLayer,
|
|
355
356
|
CreateLightLayer: () => CreateLightLayer,
|
|
356
357
|
CreateNullLayer: () => CreateNullLayer,
|
|
357
358
|
CreatePrecompLayer: () => CreatePrecompLayer,
|
|
@@ -361,6 +362,7 @@ __export(schemas_exports, {
|
|
|
361
362
|
DeleteComp: () => DeleteComp,
|
|
362
363
|
DeleteLayer: () => DeleteLayer,
|
|
363
364
|
DuplicateLayer: () => DuplicateLayer,
|
|
365
|
+
ExportMogrt: () => ExportMogrt,
|
|
364
366
|
FindLayers: () => FindLayers,
|
|
365
367
|
GUIDE_TOPICS: () => GUIDE_TOPICS,
|
|
366
368
|
GetComp: () => GetComp,
|
|
@@ -371,6 +373,7 @@ __export(schemas_exports, {
|
|
|
371
373
|
GetKeyframes: () => GetKeyframes,
|
|
372
374
|
GetLayerFull: () => GetLayerFull,
|
|
373
375
|
GetProjectSummary: () => GetProjectSummary,
|
|
376
|
+
ImportFootage: () => ImportFootage,
|
|
374
377
|
InitProject: () => InitProject,
|
|
375
378
|
Interpolation: () => Interpolation,
|
|
376
379
|
ListAvailableEffects: () => ListAvailableEffects,
|
|
@@ -427,7 +430,10 @@ var Interpolation = z2.object({
|
|
|
427
430
|
easeIn: z2.object({ influence: z2.number(), speed: z2.number() }).optional(),
|
|
428
431
|
easeOut: z2.object({ influence: z2.number(), speed: z2.number() }).optional()
|
|
429
432
|
});
|
|
430
|
-
var
|
|
433
|
+
var includeParam = (sections, hint) => z2.array(z2.enum(sections)).optional().describe(`Sections to return: ${sections.join(", ")}. Omit for all of them; [] for ${hint}.`);
|
|
434
|
+
var ListComps = z2.object({
|
|
435
|
+
include: includeParam(["size", "timing", "bg", "counts"], "id + name only")
|
|
436
|
+
}).strict();
|
|
431
437
|
var GetComp = z2.object({ compId: z2.number() });
|
|
432
438
|
var GetCompTree = z2.object({ compId: z2.number(), depth: z2.number().int().min(0).max(8).default(2).optional() });
|
|
433
439
|
var CreateComp = z2.object({
|
|
@@ -452,8 +458,18 @@ var SetComp = z2.object({
|
|
|
452
458
|
});
|
|
453
459
|
var DeleteComp = z2.object({ compId: z2.number() });
|
|
454
460
|
var SetActiveComp = z2.object({ compId: z2.number() });
|
|
455
|
-
var ListLayers = z2.object({
|
|
456
|
-
|
|
461
|
+
var ListLayers = z2.object({
|
|
462
|
+
compId: z2.number(),
|
|
463
|
+
include: includeParam(["flags", "timing", "parent"], "the id/index/name/type map alone")
|
|
464
|
+
});
|
|
465
|
+
var GetLayerFull = z2.object({
|
|
466
|
+
compId: z2.number(),
|
|
467
|
+
layerId: z2.number(),
|
|
468
|
+
includeChildren: z2.boolean().default(false).optional(),
|
|
469
|
+
include: includeParam(["transform", "effects", "masks", "markers", "bounds", "text", "shape", "source"], "the layer header alone"),
|
|
470
|
+
maxKeyframes: z2.number().int().positive().optional().describe("Cap the keyframes serialized per property. Over the cap you get the first and last few plus a count of what was omitted \u2014 never a silent truncation. Omit for all of them."),
|
|
471
|
+
shapeDepth: z2.number().int().min(0).max(4).optional().describe("How deep to walk a shape layer's Contents tree. Default 4; drop to 1-2 on heavy shape layers.")
|
|
472
|
+
});
|
|
457
473
|
var CreateTextLayer = z2.object({
|
|
458
474
|
compId: z2.number(),
|
|
459
475
|
text: z2.string().default(""),
|
|
@@ -461,7 +477,8 @@ var CreateTextLayer = z2.object({
|
|
|
461
477
|
size: z2.number().positive().optional(),
|
|
462
478
|
color: Color.optional(),
|
|
463
479
|
position: VecAny.optional(),
|
|
464
|
-
|
|
480
|
+
tracking: z2.number().optional().describe("Letter-spacing. Omit it and the layer is created with tracking 0, because AE's addText() otherwise inherits whatever the user's Character panel was last left on (-20 is common) and the same call then renders differently on two machines. Not normalised when anchorAlign is 'none'."),
|
|
481
|
+
anchorAlign: z2.enum(["left", "center", "right", "none"]).default("left").optional().describe("How the text aligns to `position`, implemented as live paragraph justification with the anchor point left at [0,0]. Default 'left' makes `position` the start of the first baseline; 'center' and 'right' put it at the centre/end. Because it is justification rather than a measured anchor offset, the alignment stays correct when the Source Text changes later \u2014 retyped, driven by an expression, or edited through Essential Graphics. 'none' leaves AE's raw defaults alone: no justification, no anchor move, no tracking reset."),
|
|
465
482
|
name: z2.string().optional()
|
|
466
483
|
});
|
|
467
484
|
var CreateShapeLayer = z2.object({
|
|
@@ -512,7 +529,12 @@ var SetLayer = z2.object({
|
|
|
512
529
|
preserveTransparency: z2.boolean().optional(),
|
|
513
530
|
trackMatte: z2.object({ type: z2.string(), layerId: z2.number().optional() }).optional()
|
|
514
531
|
});
|
|
515
|
-
var ParentLayer = z2.object({
|
|
532
|
+
var ParentLayer = z2.object({
|
|
533
|
+
compId: z2.number(),
|
|
534
|
+
layerId: z2.number(),
|
|
535
|
+
parentLayerId: z2.number().nullable(),
|
|
536
|
+
preserveTransform: z2.boolean().default(true).optional().describe("Keep the layer visually where it is (what AE's UI does). Leave on unless you want the layer to jump into the parent's coordinate space.")
|
|
537
|
+
});
|
|
516
538
|
var ReorderLayer = z2.object({ compId: z2.number(), layerId: z2.number(), toIndex: z2.number().int().positive() });
|
|
517
539
|
var SetTransform = z2.object({
|
|
518
540
|
compId: z2.number(),
|
|
@@ -592,7 +614,10 @@ var SetEffectParam = z2.object({
|
|
|
592
614
|
keyframe: z2.boolean().default(false).optional()
|
|
593
615
|
});
|
|
594
616
|
var SetEffectEnabled = z2.object({ compId: z2.number(), layerId: z2.number(), effectIndex: z2.number().int().positive(), enabled: z2.boolean() });
|
|
595
|
-
var ListAvailableEffects = z2.object({
|
|
617
|
+
var ListAvailableEffects = z2.object({
|
|
618
|
+
filter: z2.string().optional(),
|
|
619
|
+
refresh: z2.boolean().optional()
|
|
620
|
+
}).strict();
|
|
596
621
|
var SetText = z2.object({
|
|
597
622
|
compId: z2.number(),
|
|
598
623
|
layerId: z2.number(),
|
|
@@ -707,7 +732,8 @@ var AddShapeContent = z2.object({
|
|
|
707
732
|
compId: z2.number(),
|
|
708
733
|
layerId: z2.number(),
|
|
709
734
|
parentGroupPath: PropertyPath.optional(),
|
|
710
|
-
content: ShapeContent
|
|
735
|
+
content: ShapeContent,
|
|
736
|
+
zOrder: z2.enum(["front", "back"]).optional().describe("Where the new node sits in its group's render stack. Index 1 renders in FRONT, and AE appends new content to the end \u2014 so the default, 'back', puts each node behind everything already there. 'front' moves it to index 1. Prefer ordering your calls front-to-back (details first, background rects last) over reaching for 'front': it needs an internal moveTo, which has been seen to disturb nested renders of the comp in AE 26.3.")
|
|
711
737
|
});
|
|
712
738
|
var SetShapeProperty = z2.object({
|
|
713
739
|
compId: z2.number(),
|
|
@@ -758,16 +784,17 @@ var RemoveMarker = z2.object({
|
|
|
758
784
|
layerId: z2.number().optional(),
|
|
759
785
|
markerIndex: z2.number().int().positive()
|
|
760
786
|
});
|
|
787
|
+
var downsampleParam = z2.number().int().min(1).max(8).optional().describe("Render at 1/N resolution. Omit and one is chosen from the comp size (long edge ~1280px: 2 at 1080p, 3 at 4K). Pass 1 for a full-resolution frame.");
|
|
761
788
|
var ScreenshotFrame = z2.object({
|
|
762
789
|
compId: z2.number(),
|
|
763
790
|
time: z2.number().optional(),
|
|
764
|
-
downsample:
|
|
791
|
+
downsample: downsampleParam
|
|
765
792
|
});
|
|
766
793
|
var ScreenshotLayer = z2.object({
|
|
767
794
|
compId: z2.number(),
|
|
768
795
|
layerId: z2.number(),
|
|
769
796
|
time: z2.number().optional(),
|
|
770
|
-
downsample:
|
|
797
|
+
downsample: downsampleParam
|
|
771
798
|
});
|
|
772
799
|
var RunBatch = z2.object({
|
|
773
800
|
ops: z2.array(z2.object({ op: z2.string(), args: z2.unknown() })),
|
|
@@ -781,7 +808,31 @@ var FindLayers = z2.object({
|
|
|
781
808
|
type: z2.string().optional(),
|
|
782
809
|
hasEffectMatchName: z2.string().optional()
|
|
783
810
|
});
|
|
784
|
-
var RunJsx = z2.object({
|
|
811
|
+
var RunJsx = z2.object({
|
|
812
|
+
code: z2.string(),
|
|
813
|
+
undoGroup: z2.boolean().default(true).optional().describe("Wrap the script in one undo step. Set false only for the operations AE refuses while an undo group is open \u2014 copyToComp on a layer with a parent or a linked expression. The script's changes then land as whatever undo steps AE records on its own.")
|
|
814
|
+
});
|
|
815
|
+
var ImportFootage = z2.object({
|
|
816
|
+
path: z2.string().min(1).describe("Absolute path to the file to import."),
|
|
817
|
+
name: z2.string().optional().describe("Rename the project item after import. Omit to keep the filename."),
|
|
818
|
+
sequence: z2.boolean().default(false).optional().describe("Import a numbered still as an image sequence rather than a single frame."),
|
|
819
|
+
force: z2.boolean().default(false).optional().describe("Keep an item that failed validation instead of deleting it and throwing. The problem is still reported in `validation`. Only pass this when you know the dimensions are wrong and want the item anyway.")
|
|
820
|
+
}).strict();
|
|
821
|
+
var CreateFootageLayer = z2.object({
|
|
822
|
+
compId: z2.number(),
|
|
823
|
+
itemId: z2.number().describe("Project item id from import_footage or get_project_summary."),
|
|
824
|
+
name: z2.string().optional(),
|
|
825
|
+
position: VecAny.optional(),
|
|
826
|
+
startTime: z2.number().optional()
|
|
827
|
+
});
|
|
828
|
+
var ExportMogrt = z2.object({
|
|
829
|
+
compId: z2.number(),
|
|
830
|
+
destDir: z2.string().optional().describe("Folder to write the .mogrt into. Defaults to the folder holding the .aep."),
|
|
831
|
+
name: z2.string().optional().describe("Template name, which is also the output filename. Defaults to the comp name \u2014 AE's own default is the literal 'Untitled', so every scripted export would otherwise overwrite the same file."),
|
|
832
|
+
overwrite: z2.boolean().default(false).optional().describe("Required to replace an existing .mogrt at that path."),
|
|
833
|
+
posterTime: z2.number().optional().describe("Comp time to render as the template's still thumbnail, replacing the black one AE writes. Omit to leave AE's thumbnail alone."),
|
|
834
|
+
suppressDialogs: z2.boolean().default(true).optional().describe("Suppress the modal font warning during export. Leave true: an unsuppressed dialog freezes the bridge until someone clicks it in AE. Set false only to see the dialog deliberately.")
|
|
835
|
+
}).strict();
|
|
785
836
|
var GetHouseStyle = z2.object({}).strict();
|
|
786
837
|
var SetHouseStyle = z2.object({
|
|
787
838
|
content: z2.string().min(1).describe("The complete style guide as markdown. Replaces the file, so send the whole document."),
|
|
@@ -811,7 +862,10 @@ var LogIssue = z2.object({
|
|
|
811
862
|
}).strict();
|
|
812
863
|
var ListKnownIssues = z2.object({
|
|
813
864
|
status: z2.enum(["all", "unreported", "reported"]).default("all").optional(),
|
|
814
|
-
tool: z2.string().optional().describe("Only entries about this tool, e.g. 'set_temporal_ease'. Omit for everything.")
|
|
865
|
+
tool: z2.string().optional().describe("Only entries about this tool, e.g. 'set_temporal_ease'. Omit for everything."),
|
|
866
|
+
query: z2.string().optional().describe("Free-text filter: every whitespace-separated term must appear in an entry's title, symptom or tools."),
|
|
867
|
+
id: z2.string().optional().describe("Read one entry in full \u2014 cause and workaround included \u2014 by the id from a previous listing. Ignores the filters."),
|
|
868
|
+
detail: z2.enum(["index", "full"]).default("index").optional().describe("'index' (default) is one line per entry: id, title, tools, counts and a one-line summary \u2014 read the one you need with `id`. 'full' returns every matching entry's whole body and costs thousands of tokens.")
|
|
815
869
|
}).strict();
|
|
816
870
|
var MarkIssueReported = z2.object({
|
|
817
871
|
id: z2.string().describe("The entry id returned by log_issue or list_known_issues."),
|
|
@@ -888,6 +942,11 @@ var OpSchemas = {
|
|
|
888
942
|
// explore
|
|
889
943
|
get_project_summary: GetProjectSummary,
|
|
890
944
|
find_layers: FindLayers,
|
|
945
|
+
// footage
|
|
946
|
+
import_footage: ImportFootage,
|
|
947
|
+
create_footage_layer: CreateFootageLayer,
|
|
948
|
+
// motion graphics templates
|
|
949
|
+
export_mogrt: ExportMogrt,
|
|
891
950
|
// raw
|
|
892
951
|
run_jsx: RunJsx,
|
|
893
952
|
// house style
|
|
@@ -928,15 +987,69 @@ var BridgeUnreachableError = class _BridgeUnreachableError extends Error {
|
|
|
928
987
|
].filter(Boolean).join("\n");
|
|
929
988
|
}
|
|
930
989
|
};
|
|
990
|
+
var BridgeTimeoutError = class _BridgeTimeoutError extends Error {
|
|
991
|
+
constructor(port, timeoutMs, opts = {}) {
|
|
992
|
+
super(_BridgeTimeoutError.message(port, timeoutMs, opts));
|
|
993
|
+
this.port = port;
|
|
994
|
+
this.timeoutMs = timeoutMs;
|
|
995
|
+
this.name = "BridgeTimeoutError";
|
|
996
|
+
}
|
|
997
|
+
port;
|
|
998
|
+
timeoutMs;
|
|
999
|
+
static message(port, timeoutMs, opts = {}) {
|
|
1000
|
+
const secs = Math.round(timeoutMs / 1e3);
|
|
1001
|
+
const what = opts.op ? `\`${opts.op}\`` : "the call";
|
|
1002
|
+
return [
|
|
1003
|
+
`The After Effects panel at http://127.0.0.1:${port} did not answer within ${secs}s for ${what}.`,
|
|
1004
|
+
"",
|
|
1005
|
+
"This is a timeout, not a lost connection \u2014 the panel is very probably still",
|
|
1006
|
+
"there. ExtendScript is single-threaded, so while After Effects is busy running a",
|
|
1007
|
+
"script it cannot answer the bridge at all, and a busy AE looks exactly like a",
|
|
1008
|
+
"dead one from here. It usually recovers on its own.",
|
|
1009
|
+
"",
|
|
1010
|
+
"What to do, in order:",
|
|
1011
|
+
"1. Do not restart After Effects, and do not run setup_panel. Do not re-send the",
|
|
1012
|
+
" call either \u2014 that would queue a second copy of the same work.",
|
|
1013
|
+
"2. Call check_setup, and keep polling it for about a minute. The bridge normally",
|
|
1014
|
+
" comes back by itself once the script finishes.",
|
|
1015
|
+
"3. Ask the user to look at After Effects. A modal dialog \u2014 an unsaved-project",
|
|
1016
|
+
" prompt, a missing-font warning \u2014 blocks it in the same way and may be hidden",
|
|
1017
|
+
" behind another window. Only they can click it.",
|
|
1018
|
+
"4. On macOS, if they have switched to another desktop (Space), ask them to switch",
|
|
1019
|
+
" back to the one After Effects is on. Calls have been reported to stall until",
|
|
1020
|
+
" they return, then complete normally.",
|
|
1021
|
+
"5. Only if check_setup still reports the bridge down after a minute should you",
|
|
1022
|
+
" treat this as a real disconnection and follow its nextSteps.",
|
|
1023
|
+
opts.adjustable === false ? "" : `
|
|
1024
|
+
If this work legitimately takes longer than ${secs}s, the limit is settable: start the
|
|
1025
|
+
server with AE_MCP_OP_TIMEOUT_MS set to a larger number of milliseconds.`
|
|
1026
|
+
].filter(Boolean).join("\n");
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
function isTimeoutError(e, depth = 0) {
|
|
1030
|
+
if (!e || typeof e !== "object" || depth > 4) return false;
|
|
1031
|
+
const name = e.name;
|
|
1032
|
+
if (name === "TimeoutError" || name === "AbortError") return true;
|
|
1033
|
+
const cause = e.cause;
|
|
1034
|
+
return cause === e ? false : isTimeoutError(cause, depth + 1);
|
|
1035
|
+
}
|
|
931
1036
|
var AeError = class extends Error {
|
|
932
|
-
|
|
1037
|
+
/**
|
|
1038
|
+
* `code` is set only when the panel diagnosed the failure itself rather than
|
|
1039
|
+
* relaying one from ExtendScript — `STALE_FRAME` is the first. Those messages
|
|
1040
|
+
* already read as complete instructions, so the caller uses this to decide
|
|
1041
|
+
* whether an `AE:` prefix would help or just obscure them.
|
|
1042
|
+
*/
|
|
1043
|
+
constructor(message, stack_, line, code) {
|
|
933
1044
|
super(message);
|
|
934
1045
|
this.stack_ = stack_;
|
|
935
1046
|
this.line = line;
|
|
1047
|
+
this.code = code;
|
|
936
1048
|
this.name = "AeError";
|
|
937
1049
|
}
|
|
938
1050
|
stack_;
|
|
939
1051
|
line;
|
|
1052
|
+
code;
|
|
940
1053
|
};
|
|
941
1054
|
|
|
942
1055
|
// src/util/logger.ts
|
|
@@ -973,6 +1086,19 @@ function discoverPort() {
|
|
|
973
1086
|
}
|
|
974
1087
|
|
|
975
1088
|
// src/bridge/httpClient.ts
|
|
1089
|
+
var DEFAULT_OP_TIMEOUT_MS = 12e4;
|
|
1090
|
+
var SLOW_OPS = /* @__PURE__ */ new Set(["run_batch", "run_jsx", "screenshot_frame", "screenshot_layer", "export_mogrt", "import_footage"]);
|
|
1091
|
+
var SLOW_OP_TIMEOUT_MS = 3e5;
|
|
1092
|
+
function opTimeoutMs(op) {
|
|
1093
|
+
const raw = process.env.AE_MCP_OP_TIMEOUT_MS?.trim();
|
|
1094
|
+
if (raw) {
|
|
1095
|
+
const n = Number.parseInt(raw, 10);
|
|
1096
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
1097
|
+
logger.warn(`Ignoring AE_MCP_OP_TIMEOUT_MS=${raw} \u2014 expected a positive number of milliseconds.`);
|
|
1098
|
+
}
|
|
1099
|
+
return op && SLOW_OPS.has(op) ? SLOW_OP_TIMEOUT_MS : DEFAULT_OP_TIMEOUT_MS;
|
|
1100
|
+
}
|
|
1101
|
+
var HEALTH_TIMEOUT_MS = 2e3;
|
|
976
1102
|
var HttpClient = class {
|
|
977
1103
|
port;
|
|
978
1104
|
base;
|
|
@@ -984,23 +1110,28 @@ var HttpClient = class {
|
|
|
984
1110
|
// treat undefined as "too old to say" rather than as a mismatch.
|
|
985
1111
|
async health() {
|
|
986
1112
|
try {
|
|
987
|
-
const r = await fetch(`${this.base}/health`, { signal: AbortSignal.timeout(
|
|
1113
|
+
const r = await fetch(`${this.base}/health`, { signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS) });
|
|
988
1114
|
if (!r.ok) throw new Error(`health HTTP ${r.status}`);
|
|
989
1115
|
return await r.json();
|
|
990
1116
|
} catch (e) {
|
|
1117
|
+
if (isTimeoutError(e)) {
|
|
1118
|
+
throw new BridgeTimeoutError(this.port, HEALTH_TIMEOUT_MS, { op: "health", adjustable: false });
|
|
1119
|
+
}
|
|
991
1120
|
throw new BridgeUnreachableError(this.port, e);
|
|
992
1121
|
}
|
|
993
1122
|
}
|
|
994
1123
|
async runOp(op, args, progressToken) {
|
|
995
1124
|
let resp;
|
|
1125
|
+
const timeoutMs = opTimeoutMs(op);
|
|
996
1126
|
try {
|
|
997
1127
|
resp = await fetch(`${this.base}/op`, {
|
|
998
1128
|
method: "POST",
|
|
999
1129
|
headers: { "content-type": "application/json" },
|
|
1000
1130
|
body: JSON.stringify({ op, args: args ?? {}, progressToken }),
|
|
1001
|
-
signal: AbortSignal.timeout(
|
|
1131
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1002
1132
|
});
|
|
1003
1133
|
} catch (e) {
|
|
1134
|
+
if (isTimeoutError(e)) throw new BridgeTimeoutError(this.port, timeoutMs, { op });
|
|
1004
1135
|
throw new BridgeUnreachableError(this.port, e);
|
|
1005
1136
|
}
|
|
1006
1137
|
let data;
|
|
@@ -1010,7 +1141,7 @@ var HttpClient = class {
|
|
|
1010
1141
|
throw new AeError(`Bridge returned non-JSON (HTTP ${resp.status})`);
|
|
1011
1142
|
}
|
|
1012
1143
|
if (!data.ok) {
|
|
1013
|
-
throw new AeError(data.error, data.stack, data.line);
|
|
1144
|
+
throw new AeError(data.error, data.stack, data.line, data.code);
|
|
1014
1145
|
}
|
|
1015
1146
|
return data.result;
|
|
1016
1147
|
}
|
|
@@ -1192,7 +1323,7 @@ var JobManager = class {
|
|
|
1192
1323
|
// src/tools/descriptions.ts
|
|
1193
1324
|
var descriptions = {
|
|
1194
1325
|
// ---------- comps ----------
|
|
1195
|
-
list_comps: "All comps: id, name, dims, duration, fps, layer count.",
|
|
1326
|
+
list_comps: "All comps: id, name, dims, duration, fps, layer count. Pass `include` to trim it \u2014 `include: []` returns the id+name map alone, which is all orientation usually needs.",
|
|
1196
1327
|
get_comp: "Single comp summary by id (no layers).",
|
|
1197
1328
|
get_comp_tree: "Comp + nested layer tree, recursing pre-comps to `depth`.",
|
|
1198
1329
|
create_comp: "Create a new comp. Returns id.",
|
|
@@ -1200,9 +1331,9 @@ var descriptions = {
|
|
|
1200
1331
|
delete_comp: "Delete a comp. Reversible only via AE's Undo.",
|
|
1201
1332
|
set_active_comp: "Focus a comp in the viewer/timeline.",
|
|
1202
1333
|
// ---------- layers ----------
|
|
1203
|
-
list_layers: "Layers in a comp, one-line each. Use get_layer_full for details.",
|
|
1204
|
-
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.",
|
|
1205
|
-
create_text_layer: "Text layer with optional font/size/color/position. anchorAlign
|
|
1334
|
+
list_layers: "Layers in a comp, one-line each. Use get_layer_full for details. Pass `include` to trim it \u2014 `include: []` returns just id/index/name/type, the cheapest way to learn what is in a comp.",
|
|
1335
|
+
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. Bound the answer on a heavy layer: `include` picks the sections you need, `maxKeyframes` caps the keyframes per property, `shapeDepth` limits the Contents walk. Anything dropped is named and counted in the response, so a bounded read is never mistaken for a complete one.",
|
|
1336
|
+
create_text_layer: "Text layer with optional font/size/color/position/tracking. anchorAlign (default 'left') aligns the text by setting paragraph justification and leaving the anchor at [0,0], so position means the start of the baseline AND stays right when the text is changed later. Tracking is set to 0 unless you pass one, because AE otherwise inherits the user's Character panel. anchorAlign 'none' keeps AE's raw defaults.",
|
|
1206
1337
|
create_shape_layer: "Empty shape layer; fill via add_shape_content.",
|
|
1207
1338
|
create_solid_layer: "Solid-color layer. color is RGB 0..1.",
|
|
1208
1339
|
create_null_layer: "Null parent layer.",
|
|
@@ -1213,7 +1344,7 @@ var descriptions = {
|
|
|
1213
1344
|
duplicate_layer: "Duplicate a layer N times; each has a fresh id.",
|
|
1214
1345
|
delete_layer: "Remove a layer.",
|
|
1215
1346
|
set_layer: "Update layer metadata (name/enabled/locked/shy/solo/3D/blend/label/in-out/stretch/trackMatte). Undefined fields unchanged.",
|
|
1216
|
-
parent_layer: "Set/clear a layer's parent (parentLayerId=null to unparent).",
|
|
1347
|
+
parent_layer: "Set/clear a layer's parent (parentLayerId=null to unparent). The layer stays visually put: AE's own compensation double-counts when the parent was itself re-parented in the same call, so this recomputes the world transform and corrects position/scale/rotation, reporting any correction in `correction`. Read `correction.notes` \u2014 3D layers, cameras and lights are not corrected, and with a keyframed ancestor the fix is only exact at the comp's current time. Pass preserveTransform:false to let the layer jump instead.",
|
|
1217
1348
|
reorder_layer: "Move layer to 1-based stack index.",
|
|
1218
1349
|
// ---------- transforms ----------
|
|
1219
1350
|
set_transform: "Set any of position/scale/rotation/anchorPoint/opacity (+3D orientation/per-axis on 3D). keyframe:true + time sets keyframes.",
|
|
@@ -1231,17 +1362,17 @@ var descriptions = {
|
|
|
1231
1362
|
clear_expression: "Remove an expression.",
|
|
1232
1363
|
// ---------- effects ----------
|
|
1233
1364
|
list_effects: "All effects on a layer with current param values + keyframes/expressions.",
|
|
1234
|
-
add_effect: "Add effect by matchName (use list_available_effects \u2014 matchNames are stable across AE versions; display names aren't).",
|
|
1365
|
+
add_effect: "Add effect by matchName (use list_available_effects \u2014 matchNames are stable across AE versions; display names aren't). A wrong matchName fails immediately and cheaply, so try the standard one (ADBE Gaussian Blur 2, ADBE Slider Control) before searching.",
|
|
1235
1366
|
remove_effect: "Remove effect by 1-based index.",
|
|
1236
1367
|
set_effect_param: "Set an effect param by name/matchName. keyframe:true+time for keyframed value.",
|
|
1237
1368
|
set_effect_enabled: "Toggle an effect on/off without removing.",
|
|
1238
|
-
list_available_effects: "
|
|
1369
|
+
list_available_effects: "Effects installed in this AE: displayName, matchName, category. **Always pass `filter`** to substring-search: the full list is 250-450+ entries and the cost is in returning them, not in reading them, so an unfiltered call takes seconds every time while a filtered one takes a fraction of one (measured on AE 26.3: 446 entries in 3.9s, 22 filtered in 0.17s). `refresh:true` re-reads after installing a plugin; the enumeration is cached per AE session, which is why refresh exists. Never loop over `app.effects` in run_jsx: it is slow enough to block the bridge past its timeout and looks like a crash.",
|
|
1239
1370
|
// ---------- text ----------
|
|
1240
1371
|
set_text: "Set text content + styling (font, size, fill/stroke, tracking, leading, justification). Undefined fields unchanged.",
|
|
1241
1372
|
add_text_animator: "Add a text animator group (position/scale/rotation/opacity/tracking/skew/fillColor/strokeColor). Optional range selector.",
|
|
1242
1373
|
// ---------- shapes ----------
|
|
1243
1374
|
set_shape_path: "Replace a shape path with new vertices/tangents/closed.",
|
|
1244
|
-
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.",
|
|
1375
|
+
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. ORDER MATTERS: index 1 renders in FRONT and each call appends behind the last, so add front-to-back \u2014 details and dots first, big background rects last. `zOrder:'front'` can override that, but it needs an internal moveTo which has disturbed nested renders in AE 26.3, so prefer call order. A reference to one node goes stale once a sibling is added to the same group (add a Stroke and an earlier Fill reference throws 'Object is invalid'): add every node first, then address them by name.",
|
|
1245
1376
|
set_shape_property: "Set a property on a shape content node. Optional keyframe at time.",
|
|
1246
1377
|
// ---------- masks ----------
|
|
1247
1378
|
add_mask: "Add a mask with vertices/tangents/closed/mode.",
|
|
@@ -1251,15 +1382,20 @@ var descriptions = {
|
|
|
1251
1382
|
add_marker: "Add a marker on a layer (layerId) or on the comp. time + optional duration/comment/label/chapter/url/frameTarget.",
|
|
1252
1383
|
remove_marker: "Remove a marker by 1-based index.",
|
|
1253
1384
|
// ---------- vision ----------
|
|
1254
|
-
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.
|
|
1255
|
-
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.",
|
|
1385
|
+
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. A downsample is chosen from the comp size unless you pass one \u2014 omit it, and pass downsample:1 only when you genuinely need full resolution. The result reports the dimensions actually returned, and warns if a requested downsample could not be applied. Not every result is an image: a 'Stale frame' error means AE re-served an earlier render \u2014 space calls a few seconds apart, retry with a higher downsample, and confirm motion by reading keyframes instead; `empty:true` means every pixel is transparent, so check the time, in/out points and enabled state. Never disable layers to make a frame render.",
|
|
1386
|
+
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. The same 'Stale frame' and `empty:true` non-image results apply.",
|
|
1256
1387
|
// ---------- batch ----------
|
|
1257
1388
|
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.",
|
|
1258
1389
|
// ---------- explore ----------
|
|
1259
|
-
get_project_summary: "Project state: path, item count, active item, flat item list with type.",
|
|
1390
|
+
get_project_summary: "Project state: path, item count, active item, flat item list with type (comp | footage | solid | folder | unknown \u2014 same vocabulary as a layer's sourceType).",
|
|
1260
1391
|
find_layers: "Search across one or all comps for layers matching name/type/effect filters.",
|
|
1261
1392
|
// ---------- raw ----------
|
|
1262
|
-
run_jsx:
|
|
1393
|
+
run_jsx: 'Escape hatch: arbitrary ExtendScript in an undo group. `comp`/`app`/`OPS`/helpers in scope. `return X` sends a value back \u2014 arrays and nested objects come back whole. Anything that cannot be JSON is replaced in place by a marker string, never dropped: `"[function]"`, `"[undefined]"`, `"[circular]"`, `"[max depth]"`, `"[NaN]"`, and live AE objects as `"[CompItem \\"Main\\" #12]"` \u2014 a handle to pass to a real read tool, not a walk of the object. An empty result therefore means the script really returned nothing. AE refuses copyToComp for a layer with a parent or a linked expression while an undo group is open: call `withoutUndoGroup(function(){ \u2026 })` around just that part, or pass undoGroup:false for the whole script (its changes then land as whatever undo steps AE records on its own, not one). Keep loops short \u2014 ExtendScript is single-threaded and freezes the user\'s UI.',
|
|
1394
|
+
// ---------- footage ----------
|
|
1395
|
+
import_footage: "Import a file (video, image, audio, SVG, PSD/AI) into the project. Returns the item id \u2014 pass it to create_footage_layer to place it. Validates what AE actually produced: an SVG whose viewBox asks for one aspect ratio and imports at another is a known AE bug that renders as nothing with no error, so the item is deleted and the call throws with the workaround. `force:true` keeps it and reports the problem in `validation` instead.",
|
|
1396
|
+
create_footage_layer: "Place an imported project item into a comp as a layer. Takes the itemId from import_footage or get_project_summary. For a comp use create_precomp_layer instead.",
|
|
1397
|
+
// ---------- motion graphics templates ----------
|
|
1398
|
+
export_mogrt: "Export a comp as a .mogrt for Premiere. Handles the three things that make a scripted export look like a hung connection: it saves the project first (removes AE's modal save prompt), suppresses the modal font warning that otherwise freezes this connection until someone clicks OK in AE, and runs outside the undo group. `name` defaults to the comp name \u2014 AE's own default is the literal 'Untitled', so every export would otherwise overwrite the same file. Pass `posterTime` to render that frame as the template's thumbnail, replacing the black one AE writes; the export still succeeds if only the thumbnail fails. Needs the project saved once by hand first. `fonts` in the result lists the fonts the template will require \u2014 tell the user, since non-Adobe ones make Premiere flag the template.",
|
|
1263
1399
|
// ---------- house style ----------
|
|
1264
1400
|
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
1401
|
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.",
|
|
@@ -1274,7 +1410,7 @@ var descriptions = {
|
|
|
1274
1410
|
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
1411
|
init_project: "Set up a working folder for one video, series or client: a project brief and a pointer to the house style, written in whichever layout this client reads. Run it when the user is starting out or asks to set up a project. It writes files to disk \u2014 say which folder before calling, and pass `dir` explicitly unless the client already told the server where it is working. It never overwrites anything and reports every path it wrote.",
|
|
1276
1412
|
// ---------- issue journal ----------
|
|
1277
|
-
list_known_issues: "Problems earlier sessions hit with these tools, with the workarounds that worked. Read it when a tool fails in a way you don't immediately understand \u2014 pass `tool` to narrow it
|
|
1413
|
+
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` or `query` to narrow it \u2014 and before nontrivial work. It can save you rediscovering a fix that already cost someone an hour. Returns a one-line index by default; the cause and the workaround are in the entry, so follow up with `id` on anything that looks like your problem. `detail:'full'` dumps every matching entry and is rarely worth it. Also returns the repo and server version needed to report one.",
|
|
1278
1414
|
log_issue: "Record a problem you hit and the workaround that got past it, so the next session doesn't rediscover it. Log only what cost real effort and will recur: a tool failing for a non-obvious reason, an argument shape the schema didn't imply, AE behaving unlike the docs. Not your own typos, not one-off user mistakes. Call list_known_issues first and reuse the same title to extend an existing entry rather than duplicating it. If the result comes back with reported:false, then AFTER you have finished the actual work, close your reply by telling the user in plain language that something took much longer than it should have and offering to pass it to the people who maintain this tool \u2014 phrase it for a motion designer, in terms of what actually happened, and don't say 'GitHub issue' or 'bug report' unless they say it first.",
|
|
1279
1415
|
mark_issue_reported: "Record that a journal entry has been sent to the maintainers, with the resulting URL. Call it only once the issue really exists, so later sessions don't ask the user to report the same thing twice."
|
|
1280
1416
|
};
|
|
@@ -1537,6 +1673,8 @@ function debugModeLocation() {
|
|
|
1537
1673
|
}
|
|
1538
1674
|
|
|
1539
1675
|
// src/setup/check.ts
|
|
1676
|
+
var BRIDGE_PROBE_MS = 2e3;
|
|
1677
|
+
var BRIDGE_BUSY_FIX = "This is a timeout, not a refused connection \u2014 something is listening, it just did not answer in time. After Effects is most likely busy running a script, or waiting on a modal dialog nobody has clicked. Wait and run check_setup again, up to about a minute, before restarting anything; it usually clears on its own. On macOS, if the user has switched to another desktop, ask them to switch back to the one After Effects is on.";
|
|
1540
1678
|
function sha256(file) {
|
|
1541
1679
|
try {
|
|
1542
1680
|
return crypto2.createHash("sha256").update(fs5.readFileSync(file)).digest("hex");
|
|
@@ -1546,11 +1684,20 @@ function sha256(file) {
|
|
|
1546
1684
|
}
|
|
1547
1685
|
async function bridgeReachable(port) {
|
|
1548
1686
|
try {
|
|
1549
|
-
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
1687
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
1688
|
+
signal: AbortSignal.timeout(BRIDGE_PROBE_MS)
|
|
1689
|
+
});
|
|
1550
1690
|
if (!res.ok) return { ok: false, detail: `port ${port} returned HTTP ${res.status}` };
|
|
1551
1691
|
const body = await res.json().catch(() => ({}));
|
|
1552
1692
|
return { ok: true, detail: `responding on port ${port}`, bundleHash: body.bundleHash };
|
|
1553
1693
|
} catch (e) {
|
|
1694
|
+
if (isTimeoutError(e)) {
|
|
1695
|
+
return {
|
|
1696
|
+
ok: false,
|
|
1697
|
+
timedOut: true,
|
|
1698
|
+
detail: `port ${port} accepted the connection but did not answer within ${BRIDGE_PROBE_MS / 1e3}s \u2014 After Effects is probably busy`
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1554
1701
|
return { ok: false, detail: `no response on port ${port} (${e.message})` };
|
|
1555
1702
|
}
|
|
1556
1703
|
}
|
|
@@ -1620,7 +1767,7 @@ async function checkSetup() {
|
|
|
1620
1767
|
name: "bridgeReachable",
|
|
1621
1768
|
ok: bridge.ok,
|
|
1622
1769
|
detail: bridge.detail,
|
|
1623
|
-
fix: bridge.ok ? void 0 : "If the other checks pass, restart After Effects so the panel reloads."
|
|
1770
|
+
fix: bridge.ok ? void 0 : bridge.timedOut ? BRIDGE_BUSY_FIX : "If the other checks pass, restart After Effects so the panel reloads."
|
|
1624
1771
|
});
|
|
1625
1772
|
if (bridge.ok && source) {
|
|
1626
1773
|
const assessment = assessPanel(bridge.bundleHash, sha256(path6.join(installed, "jsx", "bundle.jsx")), {
|
|
@@ -1643,14 +1790,24 @@ async function checkSetup() {
|
|
|
1643
1790
|
});
|
|
1644
1791
|
}
|
|
1645
1792
|
const ready = checks.every((c) => c.ok);
|
|
1646
|
-
return { ready, checks, nextSteps: buildNextSteps(checks, ready) };
|
|
1793
|
+
return { ready, checks, nextSteps: buildNextSteps(checks, ready, bridge.timedOut === true) };
|
|
1647
1794
|
}
|
|
1648
|
-
function buildNextSteps(checks, ready) {
|
|
1795
|
+
function buildNextSteps(checks, ready, bridgeTimedOut = false) {
|
|
1649
1796
|
if (ready) return ["Everything is connected. After Effects is ready to drive."];
|
|
1650
1797
|
const by = (name) => checks.find((c) => c.name === name);
|
|
1651
1798
|
const steps = [];
|
|
1652
1799
|
if (by("platform")?.ok === false) return [by("platform").fix];
|
|
1653
1800
|
if (by("panelAssetsPresent")?.ok === false) return [by("panelAssetsPresent").fix];
|
|
1801
|
+
const installSound = by("panelInstalled")?.ok !== false && by("panelUpToDate")?.ok !== false && by("panelDependencies")?.ok !== false && by("cepDebugMode")?.ok !== false;
|
|
1802
|
+
if (bridgeTimedOut && installSound) {
|
|
1803
|
+
return [
|
|
1804
|
+
"Wait \u2014 do not restart anything yet. The panel is answering its port but is too busy to reply, which almost always means After Effects is still running a script.",
|
|
1805
|
+
"Run check_setup again in about ten seconds, and keep checking for up to a minute. It usually clears on its own.",
|
|
1806
|
+
"While waiting, ask the user to look at After Effects: a dialog it is waiting on (unsaved project, missing fonts) blocks it the same way and may be hidden behind another window.",
|
|
1807
|
+
"On macOS, if they have moved to a different desktop, ask them to switch back to the one After Effects is on.",
|
|
1808
|
+
"Only if it is still not answering after a minute should you treat it as disconnected and quit and reopen After Effects."
|
|
1809
|
+
];
|
|
1810
|
+
}
|
|
1654
1811
|
const brokenInstall = by("panelUpToDate")?.ok === false || by("panelDependencies")?.ok === false;
|
|
1655
1812
|
const needsInstall = by("panelInstalled")?.ok === false || brokenInstall;
|
|
1656
1813
|
const needsDebug = by("cepDebugMode")?.ok === false;
|
|
@@ -1772,12 +1929,12 @@ var GUIDES = [
|
|
|
1772
1929
|
{
|
|
1773
1930
|
name: "ae-setup",
|
|
1774
1931
|
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.",
|
|
1775
|
-
body:
|
|
1932
|
+
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## A timeout is not a disconnection\n\nBefore you start any repair, check which failure you actually have. "The panel did not answer within N seconds" and "cannot reach the panel" are opposite diagnoses:\n\n- **Did not answer** \u2014 something is listening; it is just too busy to reply. After Effects is single-threaded, so a long script or a modal dialog waiting for a click blocks it completely. Nothing is broken and nothing needs installing.\n- **Cannot reach** \u2014 nothing is listening. That is the case the repair path below is for.\n\nOn a timeout, `check_setup` says so itself: `bridgeReachable` reports that the port accepted the connection but did not answer in time, and `nextSteps` tells you to wait. Follow it. Re-running `setup_panel` or restarting After Effects here costs the user their work-in-progress for nothing, and both are the wrong move. Poll `check_setup` for about a minute; it usually clears on its own.\n\nTwo things to ask about while waiting: whether a dialog is sitting behind another window in After Effects, and \u2014 on macOS \u2014 whether they have switched to another desktop. Calls have been reported to stall while the user is on a different Space and to complete as soon as they come back.\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. Read the detail: if the port **timed out**, After Effects is busy and you should wait, not restart. If nothing is listening at all, restarting AE almost always fixes it. |\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.'
|
|
1776
1933
|
},
|
|
1777
1934
|
{
|
|
1778
1935
|
name: "after-effects",
|
|
1779
1936
|
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.",
|
|
1780
|
-
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.'
|
|
1937
|
+
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### Ask for what you need\n\nA tool result stays in your context for the rest of the session, so a read you cannot bound is paid for on every later call. All of these reads take an `include` list:\n\n- `list_comps` / `list_layers` with `include: []` return the id-to-name map alone, which is what orientation actually needs.\n- `get_layer_full` takes `include` (`transform`, `effects`, `masks`, `markers`, `bounds`, `text`, `shape`, `source`), plus `maxKeyframes` to cap the keyframes per property and `shapeDepth` to limit the Contents walk on a heavy shape layer.\n\nOmit them all and you get everything, as before. Whatever they leave out is named and counted in the response \u2014 a bounded read never looks like a complete one.\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\nThe same trap bites inside `run_jsx`: a `comp.layer(1)` wrapper is index-bound, not a handle. After a `copyToComp` inserts the copy at index 1, a reference you took earlier silently resolves to the *new* layer \u2014 which is how a script ends up parenting a layer to itself. Re-resolve by id or name after anything that inserts a layer.\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- **The `downsample` is picked from the comp size** unless you pass one \u2014 2 at 1080p, 3 at 4K, aiming at a long edge around 1280px. Pass `downsample: 1` only when you genuinely need full resolution: a full 4K frame is large enough to blow out your context in one call.\n- The result reports the dimensions actually returned and the factor actually applied \u2014 trust those numbers rather than assuming.\n- **Space them out.** Rapid back-to-back requests are far more likely to come back stale than requests a few seconds apart.\n\nTwo results are not images, and both are information rather than something to retry blindly:\n\n- **`Stale frame` (an error)** \u2014 After Effects returned the pixels it had already rendered for a *different* request, which the error names. Pause a few seconds and retry with a higher `downsample`; `6` has worked where `3`\u2013`4` stayed stale. If it repeats, read the keyframes instead.\n- **`empty: true`** \u2014 every pixel at that time is fully transparent, so no image was sent. That is a fact about the composition: usually the wrong time, a layer outside its in/out points, disabled, or at zero opacity.\n\n**Never disable layers to make a screenshot render.** A frame that will not render is a limit of the panel\'s render path, not project content that needs fixing \u2014 and it is very easy to leave someone\'s comp switched off afterwards.\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({filter: "blur"})` \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**Never enumerate `app.effects` yourself in `run_jsx`.** There are around 250 of them and reading the table is slow enough to block the bridge past its timeout, which looks exactly like a crash and costs a minute of everyone\'s time. `list_available_effects` does the same enumeration once and caches it for the session, so `filter` searches are free after the first call. A wrong matchName also fails instantly and clearly, so trying `ADBE Slider Control` is cheaper than searching for it.\n\n## Text\n\n`create_text_layer` defaults to `anchorAlign: "left"`, which sets **paragraph justification** and leaves the anchor point at `[0,0]`, so `position` is the start of the first baseline. Pass `"center"` or `"right"` for those, `"none"` for AE\'s raw behaviour. Because the alignment is justification rather than a measured offset, it stays correct when the text changes later \u2014 retyped, driven by an expression, or edited through Essential Graphics in Premiere. Never "fix" alignment by writing an anchor point computed from `sourceRectAtTime()`: it is right once and wrong from the next edit onward.\n\nTracking is set to `0` unless you pass one, because AE\'s `addText()` otherwise inherits whatever the user\'s Character panel was last left on.\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**Render order is the opposite of the layer stack.** Inside `Contents`, index 1 renders in *front*, and each `add_shape_content` call appends behind the previous one. So build **front-to-back**: details, text plates and traffic-light dots first, the big background rectangle last. Getting it backwards is silent \u2014 no error, just a solid slab where your artwork should be. `zOrder: "front"` will place a node at index 1 for you, but it needs an internal `moveTo`, which has been seen to disturb *nested* renders of the comp in AE 26.3; prefer ordering your calls. If an existing layer is already in the wrong order, rebuild it rather than reordering, and verify with a screenshot of a comp that **nests** it, not just the comp that owns it.\n\n**Node references go stale.** Adding a sibling to a group invalidates a reference you already hold to another node in it \u2014 add a Stroke and an earlier Fill reference starts throwing `Object is invalid`. Add every node first, then set values and expressions by addressing nodes by name.\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\nExtendScript is **single-threaded**, so a long synchronous loop freezes the user\'s AE UI. Keep the script short.\n\n`return X` sends the whole value back \u2014 arrays and nested objects included. Values that cannot be represented (functions, live AE objects, cycles) come back as a marker string in place, never dropped \u2014 a live object as `"[AVLayer \\"Hero\\" #616]"`, which is a handle to pass to `get_layer_full`, not a copy of the layer. So an empty result genuinely means the script returned nothing; never read one as "nothing happened".\n\nAE refuses `copyToComp` for a layer with a parent or a linked expression **while an undo group is open**, which is exactly the rig you wanted to copy. Wrap that one call in `withoutUndoGroup(function () { \u2026 })`, or pass `undoGroup: false` for the whole script. Nothing rolls back on error, so a script that fails halfway leaves its earlier changes applied \u2014 read the state back before re-running one that mutates.\n\nSet the parent first and the transform after, never the reverse. `parent_layer` keeps the layer where it is; raw `layer.parent = x` inside a script does not do so reliably two levels deep, so after scripted parenting audit scale and rotation as well as position.\n\n### Exporting a Motion Graphics template\n\nUse **`export_mogrt`**. Do not drive `comp.exportAsMotionGraphicsTemplate` from `run_jsx` \u2014 the tool exists because that call raises modal dialogs, and a modal dialog freezes this whole connection until someone clicks it in After Effects.\n\n`export_mogrt` handles all of it: it saves the project first (which is what removes AE\'s "the project needs to be saved" prompt, and it has to happen per export because exporting dirties the project again), it suppresses the font warning, and it runs outside the undo group so there is no "undo group mismatch" afterwards. Measured on 26.3: suppressed, an export of a comp using a non-Adobe font returns in about three seconds; unsuppressed, the same export sat past sixty and wrote nothing until the dialog was clicked.\n\nThree things worth knowing before you call it:\n\n- **The project must have been saved once, by hand.** There is no folder to save into otherwise, and the tool refuses rather than raising a dialog the user was not expecting.\n- **`name` is the filename.** It defaults to the comp name, because AE\'s own default is the literal `Untitled` \u2014 leave it to AE and every template in the project overwrites the same file.\n- **`fonts` in the result lists what the template will require.** Tell the user about any non-Adobe ones: Premiere flags the template as needing fonts it cannot supply, and that is worth hearing from you rather than discovering later.\n\n**The thumbnail.** AE writes the comp\'s *first frame* into the template, so anything that fades up from nothing gets a black one. Pass `posterTime` with a moment that actually shows the design and it is rendered and swapped in. If only the thumbnail fails the export still succeeds \u2014 check `thumbnail.patched` in the result.\n\nAlso note that `comp.setMotionGraphicsControllerName(index, \u2026)` numbers controllers in **reverse order of addition**: index 1 is the one you added last.\n\n**If any long call seems to have hung, assume a dialog before you assume a crash** \u2014 it may be behind another window. `comp.saveFrameToPng(...)` from `run_jsx` raises the save prompt the same way; use `screenshot_frame`, which does not.\n\n### Importing footage, and the SVG trap\n\nUse **`import_footage`**, then **`create_footage_layer`** to place the item in a comp. (For a comp as a layer, `create_precomp_layer`.)\n\n`import_footage` checks what AE actually produced, because one case fails silently: an SVG with a very large `viewBox` (say `0 0 278050 333334`) imports with **fabricated dimensions and renders as nothing**, no error at any stage. Verified on 26.3 \u2014 that viewBox yields a 15906x5654 item that will not even rasterize. The tool compares the aspect ratio the file asks for against the one AE produced, and on a mismatch it deletes the item and throws, rather than handing you an asset that looks healthy in the project panel and renders empty.\n\nIf you hit that, the workarounds are:\n\n- **Simple flat SVGs** \u2014 rebuild the path as a shape layer with the real vertices, scaled down to a sane coordinate space (divide by `333.334` for a 1000px version), set the fill from the SVG, and set `ADBE Vector Fill Rule` to `2` when the SVG says `fill-rule="evenodd"`. Done this way the result is pixel-accurate.\n- **Complex SVGs** \u2014 rasterise to PNG outside AE, or normalise the `viewBox` to a small coordinate space before importing.\n\n`force: true` keeps the item and reports the problem in `validation` instead of throwing. It is for when you know the dimensions are wrong and want it anyway \u2014 not a way past the error.\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. It comes back as a one-line\nindex, so open the entry that looks like your failure with\n`list_known_issues({id})` \u2014 the cause and the workaround are in the entry, not in\nthe index. `tool` and `query` narrow it further.\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.\n\n**A timeout is not proof the bridge is dead.** The error that says the panel did not answer in time is a different thing from the one that says the panel cannot be reached. Because ExtendScript is single-threaded, a busy After Effects cannot answer anything \u2014 so a long script, or a modal dialog nobody has clicked, is indistinguishable from a crash at this layer. It normally recovers on its own within a minute.\n\nSo when a call times out: do not re-send it (you would queue the same work twice), do not restart After Effects, and do not run `setup_panel`. Poll `check_setup` for about a minute first. Two causes worth asking about directly:\n\n- **A dialog is waiting.** Ask the user to check After Effects for a prompt hiding behind another window.\n- **They changed desktop.** On macOS, calls have been reported to stall while the user is on a different Space and to complete as soon as they return. If they have wandered off, ask them to switch back to the desktop After Effects is on before you diagnose anything else.\n\nIf a specific operation of yours legitimately needs longer than the limit, the user can raise it by setting `AE_MCP_OP_TIMEOUT_MS` in the server\'s environment.'
|
|
1781
1938
|
},
|
|
1782
1939
|
{
|
|
1783
1940
|
name: "style-guide",
|
|
@@ -1802,7 +1959,7 @@ var PROMPTS = [
|
|
|
1802
1959
|
name: "report-ae-issue",
|
|
1803
1960
|
description: "Send a problem you hit with the After Effects tools to the people who maintain them",
|
|
1804
1961
|
argumentHint: "[what went wrong, in your own words]",
|
|
1805
|
-
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
|
|
1962
|
+
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 a one-line index\nof what earlier sessions wrote down, plus `repo`, `newIssueUrl`, `serverVersion`\nand `platform`. Once they have chosen, read each chosen entry in full with\n`list_known_issues({id})` \u2014 the draft below needs the symptom and workaround\ntext, which the index does not carry.\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.'
|
|
1806
1963
|
}
|
|
1807
1964
|
];
|
|
1808
1965
|
var GUIDE_NAMES = GUIDES.map((g) => g.name);
|
|
@@ -1812,7 +1969,7 @@ function getGuide(name) {
|
|
|
1812
1969
|
function getPrompt(name) {
|
|
1813
1970
|
return PROMPTS.find((p) => p.name === name);
|
|
1814
1971
|
}
|
|
1815
|
-
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,
|
|
1972
|
+
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, never per frame.\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.";
|
|
1816
1973
|
|
|
1817
1974
|
// src/issues/journal.ts
|
|
1818
1975
|
import fs7 from "node:fs";
|
|
@@ -1973,39 +2130,84 @@ function logIssue(input) {
|
|
|
1973
2130
|
issueUrl: entry.issueUrl
|
|
1974
2131
|
};
|
|
1975
2132
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
2133
|
+
var SUMMARY_CHARS = 160;
|
|
2134
|
+
function summarize(entry) {
|
|
2135
|
+
const text = oneLine(entry.symptom || entry.workaround || "");
|
|
2136
|
+
return text.length > SUMMARY_CHARS ? `${text.slice(0, SUMMARY_CHARS).trimEnd()}\u2026` : text;
|
|
2137
|
+
}
|
|
2138
|
+
function toIndexEntry(e) {
|
|
2139
|
+
return {
|
|
2140
|
+
id: e.id,
|
|
2141
|
+
title: e.title,
|
|
2142
|
+
tools: e.tools,
|
|
2143
|
+
lastSeen: e.lastSeen,
|
|
2144
|
+
occurrences: e.occurrences,
|
|
2145
|
+
reported: e.reported,
|
|
2146
|
+
summary: summarize(e)
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
function readAllEntries() {
|
|
1978
2150
|
const dir = journalDir();
|
|
1979
|
-
let entries = [];
|
|
1980
2151
|
try {
|
|
1981
|
-
|
|
2152
|
+
return fs7.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => readEntry(path8.join(dir, f))).filter((e) => e !== null);
|
|
1982
2153
|
} catch {
|
|
1983
|
-
|
|
2154
|
+
return [];
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
function matchesQuery(entry, terms) {
|
|
2158
|
+
const haystack = `${entry.title} ${entry.symptom} ${entry.tools.join(" ")}`.toLowerCase();
|
|
2159
|
+
return terms.every((t) => haystack.includes(t));
|
|
2160
|
+
}
|
|
2161
|
+
function listIssues(options = {}) {
|
|
2162
|
+
const { scope } = journalRoot();
|
|
2163
|
+
const dir = journalDir();
|
|
2164
|
+
const entries = readAllEntries();
|
|
2165
|
+
const envelope = {
|
|
2166
|
+
dir,
|
|
2167
|
+
scope,
|
|
2168
|
+
repo: REPO,
|
|
2169
|
+
newIssueUrl: NEW_ISSUE_URL,
|
|
2170
|
+
serverVersion: packageVersion(),
|
|
2171
|
+
platform: process.platform
|
|
2172
|
+
};
|
|
2173
|
+
const wantedId = options.id?.trim();
|
|
2174
|
+
if (wantedId) {
|
|
2175
|
+
const found = entries.find((e) => e.id === slugify(wantedId));
|
|
2176
|
+
if (!found) {
|
|
2177
|
+
throw new Error(
|
|
2178
|
+
`No journal entry with id "${wantedId}".` + (entries.length > 0 ? ` Known ids: ${entries.map((e) => e.id).join(", ")}` : " The journal is empty.")
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
return { ...envelope, detail: "full", count: 1, issues: [found] };
|
|
1984
2182
|
}
|
|
1985
|
-
const
|
|
2183
|
+
const status = options.status ?? "all";
|
|
2184
|
+
const wanted = options.tool?.trim().toLowerCase();
|
|
2185
|
+
const terms = (options.query ?? "").toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
1986
2186
|
const filtered = entries.filter((e) => {
|
|
1987
2187
|
const byStatus = status === "all" ? true : status === "reported" ? e.reported : !e.reported;
|
|
1988
2188
|
if (!byStatus) return false;
|
|
2189
|
+
if (terms.length > 0 && !matchesQuery(e, terms)) return false;
|
|
1989
2190
|
if (!wanted) return true;
|
|
1990
2191
|
return e.tools.some((t) => t.toLowerCase() === wanted) || e.title.toLowerCase().includes(wanted);
|
|
1991
2192
|
});
|
|
1992
2193
|
filtered.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen) || b.occurrences - a.occurrences);
|
|
2194
|
+
const detail = options.detail ?? "index";
|
|
1993
2195
|
return {
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
repo: REPO,
|
|
1997
|
-
newIssueUrl: NEW_ISSUE_URL,
|
|
1998
|
-
serverVersion: packageVersion(),
|
|
1999
|
-
platform: process.platform,
|
|
2196
|
+
...envelope,
|
|
2197
|
+
detail,
|
|
2000
2198
|
count: filtered.length,
|
|
2001
|
-
issues: filtered
|
|
2199
|
+
issues: detail === "full" ? filtered : filtered.map(toIndexEntry),
|
|
2200
|
+
// The reason to read this journal is that something failed, so an index
|
|
2201
|
+
// that stopped short of the workaround would be worse than useless. Say
|
|
2202
|
+
// how to reach it, every time there is one to reach.
|
|
2203
|
+
...detail === "index" && filtered.length > 0 ? { next: 'list_known_issues({ id: "<id>" }) for the cause and the workaround.' } : {}
|
|
2002
2204
|
};
|
|
2003
2205
|
}
|
|
2004
2206
|
function markReported(id, url) {
|
|
2005
2207
|
const file = entryPath(slugify(id));
|
|
2006
2208
|
const entry = fs7.existsSync(file) ? readEntry(file) : null;
|
|
2007
2209
|
if (!entry) {
|
|
2008
|
-
const known =
|
|
2210
|
+
const known = readAllEntries().map((e) => e.id);
|
|
2009
2211
|
throw new Error(
|
|
2010
2212
|
`No journal entry with id "${id}".` + (known.length > 0 ? ` Known ids: ${known.join(", ")}` : "")
|
|
2011
2213
|
);
|
|
@@ -2049,7 +2251,7 @@ var GetJobSchema = schemas_exports.GetJob;
|
|
|
2049
2251
|
var CancelJobSchema = schemas_exports.CancelJob;
|
|
2050
2252
|
function createServer() {
|
|
2051
2253
|
const server = new Server(
|
|
2052
|
-
{ name: "after-effects-mcp", version: "0.
|
|
2254
|
+
{ name: "after-effects-mcp", version: "0.3.0" },
|
|
2053
2255
|
{
|
|
2054
2256
|
capabilities: { tools: {}, logging: {}, prompts: {}, resources: {} },
|
|
2055
2257
|
// Clients that honour this fold it into the system prompt, which is the
|
|
@@ -2176,7 +2378,17 @@ function createServer() {
|
|
|
2176
2378
|
}
|
|
2177
2379
|
if (name === "list_known_issues") {
|
|
2178
2380
|
const a = schemas_exports.ListKnownIssues.parse(rawArgs);
|
|
2179
|
-
return textResult(
|
|
2381
|
+
return textResult(
|
|
2382
|
+
listIssues({
|
|
2383
|
+
status: a.status ?? "all",
|
|
2384
|
+
tool: a.tool,
|
|
2385
|
+
query: a.query,
|
|
2386
|
+
id: a.id,
|
|
2387
|
+
// Compact unless asked otherwise: the full corpus is thousands of
|
|
2388
|
+
// tokens that stay in the transcript for the rest of the session.
|
|
2389
|
+
detail: a.detail ?? "index"
|
|
2390
|
+
})
|
|
2391
|
+
);
|
|
2180
2392
|
}
|
|
2181
2393
|
if (name === "mark_issue_reported") {
|
|
2182
2394
|
const a = schemas_exports.MarkIssueReported.parse(rawArgs);
|
|
@@ -2210,6 +2422,7 @@ function createServer() {
|
|
|
2210
2422
|
}
|
|
2211
2423
|
return textResult({ jobId: env.jobId, async: true, total: env.total });
|
|
2212
2424
|
}
|
|
2425
|
+
if (VISION_OPS.has(name) && isEmptyFrameResult(result)) return textResult(result);
|
|
2213
2426
|
if (VISION_OPS.has(name) && isVisionResult(result)) {
|
|
2214
2427
|
const v = result;
|
|
2215
2428
|
return imageContent(
|
|
@@ -2223,6 +2436,10 @@ function createServer() {
|
|
|
2223
2436
|
compId: v.compId,
|
|
2224
2437
|
layerId: v.layerId,
|
|
2225
2438
|
bytes: v.bytes,
|
|
2439
|
+
// Present only when the project renders deeper than 8 bits per
|
|
2440
|
+
// channel and the panel re-encoded the frame so it would decode.
|
|
2441
|
+
converted: v.converted,
|
|
2442
|
+
sourceBitDepth: v.sourceBitDepth,
|
|
2226
2443
|
// Surfaced when a requested downsample could not be applied, so the
|
|
2227
2444
|
// agent knows it is looking at a full-resolution frame.
|
|
2228
2445
|
warning: v.warning
|
|
@@ -2232,8 +2449,10 @@ function createServer() {
|
|
|
2232
2449
|
}
|
|
2233
2450
|
return textResult(result);
|
|
2234
2451
|
} catch (e) {
|
|
2452
|
+
if (e instanceof BridgeTimeoutError) return errorResult(e.message);
|
|
2235
2453
|
if (e instanceof BridgeUnreachableError) return errorResult(e.message);
|
|
2236
2454
|
if (e instanceof AeError) {
|
|
2455
|
+
if (e.code) return errorResult(e.message);
|
|
2237
2456
|
if (/^Unknown op: /.test(e.message)) {
|
|
2238
2457
|
panelGate.invalidate();
|
|
2239
2458
|
return errorResult(unknownOpMessage(name));
|
|
@@ -2318,6 +2537,9 @@ function isAsyncEnvelope(v) {
|
|
|
2318
2537
|
function isVisionResult(v) {
|
|
2319
2538
|
return !!(v && typeof v === "object" && "base64" in v && "width" in v);
|
|
2320
2539
|
}
|
|
2540
|
+
function isEmptyFrameResult(v) {
|
|
2541
|
+
return !!(v && typeof v === "object" && v.empty === true && !("base64" in v));
|
|
2542
|
+
}
|
|
2321
2543
|
|
|
2322
2544
|
// src/index.ts
|
|
2323
2545
|
var USAGE = `@engine-room/after-effects-mcp \u2014 drive Adobe After Effects from an AI agent.
|
|
@@ -2359,7 +2581,7 @@ ${USAGE}`);
|
|
|
2359
2581
|
await server.connect(transport);
|
|
2360
2582
|
logger.info("MCP server running on stdio");
|
|
2361
2583
|
}
|
|
2362
|
-
var VERSION = "0.
|
|
2584
|
+
var VERSION = "0.3.0";
|
|
2363
2585
|
main().catch((e) => {
|
|
2364
2586
|
logger.error("fatal", e.message);
|
|
2365
2587
|
process.exit(1);
|