Skip to content

Add shape path, mask, render queue and 3D tools; fix broken and data-losing tools - #5

Merged
ishu86 merged 27 commits into
ishu86:mainfrom
Jrocchetti:pr/shape-mask-render-3d-tools
Oct 8, 2026
Merged

ishu86 merged 27 commits into
ishu86:mainfrom
Jrocchetti:pr/shape-mask-render-3d-tools

Conversation

@Jrocchetti

@Jrocchetti Jrocchetti commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add shape-path, mask, render-queue, and 3D tools, and expose previously unregistered project, timeline, and expression generators (102 tools total).
  • Fix expression integration, subframe retiming, frame-render completion, and mask cleanup; preserve the existing bounce parameters and expose gravity-based rebounds as bounceBack.
  • Integrate the exception-safe command execution merged in PR #4.
  • Validate the 35 newly exposed tools against their advertised schemas before AE execution, including selectors, RGB/XY values, renderer names, and two-axis feathering. Preserve legacy tool coercion and support friendly aliases in batch expressions.
  • Restrict native render controls to synchronous start and showWindow; document that timeouts do not cancel rendering, live interruption/polling is unavailable, and AME is preferred for long jobs.
  • Add regression and dispatch coverage, portable Node 18/22 Linux/Windows CI, and a patched MCP SDK minimum version.
  • Local automated checks and a real MCP stdio validation smoke check pass. Live AE/AME validation of the updated head remains outstanding; earlier contributor validation used AE 26.5 on Windows.

Jrocchetti and others added 27 commits September 16, 2026 10:09
Clears 8 advisories (5 high) reaching the tree through
@modelcontextprotocol/sdk's transitive dependencies - hono,
@hono/node-server, ajv, fast-uri, body-parser. SDK moves 1.0 -> 1.30.

Semver-compatible only. Build is clean and the tool count is unchanged
at 67 before the shape work lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the shape-layer capability the server was missing. Previously it
could only create parametric Rect/Ellipse/Star primitives - never an
actual editable path, and none of the shape operators.

New tools (67 -> 71):

  create_path         editable bezier path; creates a shape layer or
                      adds to an existing one
  get_path            reads vertices/tangents/closed back, including
                      every keyframe when the path is animated
  set_path_keyframes  animates a path
  add_shape_operator  all 12 operators with friendly property names

Notes on three traps this encodes, all verified against AE 26.5:

- Path keyframes must be written one at a time with setValueAtTime().
  The bulk setValuesAtTimes() does not work on Shape-valued properties,
  only on numeric ones.

- addProperty() on a PropertyGroup invalidates references already held
  to that group's other children. Adding a Fill/Stroke after creating a
  path group makes the earlier handle throw "ReferenceError: Object is
  invalid" on something as harmless as .name, so the path name is read
  into a plain variable before any siblings are added.

- Pucker & Bloat is "ADBE Vector Filter - PB". The plausible-looking
  "ADBE Vector Filter - PuckerBloat" is not a valid match name and
  hard-crashes After Effects with no exception and no dialog. Operator
  names are validated in TypeScript so an invalid one can never reach
  AE.

Gradient tools document that color stops cannot be set by scripting:
After Effects exposes ADBE Vector Grad Colors as NO_VALUE.

Also replaces every English display-name property lookup in
add_shape_layer with match names - property("Size") returns null on a
non-English After Effects and the script then dies - and exposes
rectangle roundness, which was already in the tree but unused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The server exported 81 script generators but only wired 71 of them to
MCP tools. These ten were fully implemented and simply unreachable:

  list_project_items      set_active_composition
  set_proxy               remove_proxy
  get_current_time        set_current_time
  snap_to_marker          get_nearest_marker
  navigate_markers        batch_set_expressions

No new ExtendScript - this is schema and registration only. Tool count
71 -> 81, orphaned generators 10 -> 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The server had no render queue coverage at all. render_frame grabs a
single still, so there was no way to actually deliver anything.

  add_to_render_queue        queue a comp, with templates, output path
                             and time span
  list_render_queue          every item with status, span and output
                             modules - poll this for progress
  list_render_templates      the render settings and output module
                             presets configured in this AE install, so
                             renders reuse existing presets by name
  set_render_queue_item      retarget output, templates, span, or
                             whether an item is queued
  remove_from_render_queue   drop one item or clear the queue
  control_render             start / stop / pause / resume / showWindow
  queue_in_ame               hand off to Adobe Media Encoder

Two behaviours callers need to know, both documented on the tools:

- RenderQueue.render() is synchronous. After Effects blocks until the
  whole queue finishes, so control_render "start" will usually exceed
  the bridge's command timeout on any real render. queue_in_ame returns
  immediately because AME renders out of process, which makes it the
  better choice for long jobs.

- The output module decides the container. Passing a .mov path while
  the module is set to H.264 yields a .mp4, so the resolved path is
  returned rather than echoing what was asked for.

RQItemStatus is mapped to readable strings; the raw enum crosses the
bridge as an opaque number.

Verified end to end against AE 26.5: queue, edit, inspect, pause,
resume and clear. render() and queue_in_ame were deliberately not
executed - both tie up the machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Masks had no coverage at all. They reuse the Shape object that shape
layer paths use, so path building, validation and tangent handling are
shared with shapeGenerators rather than reimplemented - validatePathPoints,
zeroTangents and emitShapeObject are now exported for that.

  add_mask              add a mask, optionally with a bezier path and
                        full settings in one call
  list_masks            every mask with mode, feather, opacity,
                        expansion and whether its path is animated
  get_mask_path         read a path back, keyframes included
  set_mask_path         replace a path
  set_mask_keyframes    animate a path
  set_mask_properties   change settings without touching the path
  delete_mask           remove a mask

Match names verified empirically against AE 26.5, since the scripting
guide documents only ADBE Mask Parade for this group:

  ADBE Mask Shape    path       (Shape-valued)
  ADBE Mask Feather  feather    ([x, y] - a single number is applied to
                                 both axes for convenience)
  ADBE Mask Opacity  opacity
  ADBE Mask Offset   expansion  - note the name does not match the UI

set_mask_properties temporarily unlocks a locked mask, applies the
changes, then restores the requested lock state. A locked mask silently
rejects edits otherwise, which is an easy way to lose work without an
error.

Mask modes are validated in TypeScript against the MaskMode enum, so an
invalid mode is rejected before any script reaches After Effects.

Verified end to end against AE 26.5: create with a bezier path, list,
read back, animate with 3 keys, edit through a lock, delete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ht tools

3D coverage was two shallow tools - add_camera_layer took type and zoom,
add_light_layer took type, colour and intensity. Live After Effects
exposes 15 camera and 14 light properties.

  set_comp_renderer      switch the comp's 3D renderer
  get_3d_info            renderer, available renderers, and a layer's
                         current Material/Camera/Light/Geometry values
  set_3d_layer           3D switch plus orientation and rotation X/Y/Z
  set_material_options   all 17 material properties
  set_geometry_options   extrusion/bevel and plane curvature/segments
  set_camera_options     all 15 camera properties
  set_light_options      all 14 light properties

The hard part is that layer.property(matchName) returns a live object
even when the property cannot be set - setValue then throws "the
property or a parent property is hidden" - and .active, .elided and
.enabled all report identical values in the working and failing cases.
There is no flag to test first.

So setters apply properties individually and report applied[] and
failed[] rather than aborting on the first refusal, and each hidden
failure is annotated with availableUnderRenderers naming the renderers
that actually expose it. Extrusion additionally fails fast with an
actionable message, since it is the common case.

Renderers are NOT a hierarchy - each exposes a different subset,
measured on AE 26.5:

  Blur Level, Iris, Highlights      classic only
  Depth of Field, Focus, Aperture   classic + advanced
  Focus Area Width, Shadow Colour   advanced only
  Appears in Reflections            cinema4d only

A first pass told callers to switch to "advanced" while they were
already on it. Naming the specific renderer per property replaced that.

Note that "ADBE Advanced 3d" is the CLASSIC renderer despite its name,
and is the default for new comps. Two more name traps encoded: cone
feather is "ADBE Light Cone Feather 2", and highlight saturation is
"ADBE Iris Hightlight Saturation" - Adobe's own typo, which must be
spelled wrong to work.

Verified end to end against AE 26.5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…al easing

Found by a full regression sweep over every non-destructive tool.

Data loss in offset_keyframes and scale_keyframe_timing
------------------------------------------------------
Both removed EVERY key first and then re-added only the ones they could
place, silently dropping the rest while still reporting success.

  - scale_keyframe_timing with scale missing -> scaleFactor undefined ->
    every new time NaN -> `NaN >= 0` false -> all keys removed, none
    re-added, response "keyframesScaled: 2". Passing `factor` instead of
    `scale` - an easy slip - wiped the animation.
  - scale 0 collapsed every key onto one frame.
  - offset_keyframes silently deleted any key pushed before time 0.

Both now compute and validate every new time BEFORE removing anything,
and refuse with "Nothing was changed" if a key would be lost - landing
before 0, or (for scale) two keys merging onto the same frame. Numeric
params are validated in TypeScript first. The reported count is now the
number of keys actually written, not the number that were read.

Spatial properties take one temporal ease
-----------------------------------------
set_keyframe_advanced, apply_easy_ease and set_temporal_ease all counted
TwoD_SPATIAL / ThreeD_SPATIAL as 2 or 3 dimensions and built that many
KeyframeEase objects. Spatial properties (Position, Anchor Point) take
exactly one, so all three failed on Position - easy easing Position being
apply_easy_ease's main use - with "Value array does not have 1 elements".
Only non-spatial multi-dimensional properties such as Scale get 2 or 3.

copy_keyframes runaway
----------------------
The loop read sourceProp.numKeys live. Copying onto the same property
grew the bound with every key added: a 2-key copy produced 4,321 keys.
Source keys are now snapshotted before any are written. Also validates
sourceProperty, which previously crashed in TypeScript with
"Cannot read properties of undefined (reading 'split')".

Verified against AE 26.5: all eight bad-input cases are refused with the
keyframes byte-identical afterwards, and valid input still produces the
expected times.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_current_time, navigate_markers, collect_files and import_folder all
ended their generated script with a bare object literal:

    {
      time: comp.time,
      frame: Math.round(comp.time * comp.frameRate)
    };

In statement position a `{` opens a BLOCK, not an object. `time:` parses
as a label and the comma is a syntax error, so every call failed with
"SyntaxError: Expected: ;" - for every user, on every invocation.

get_current_time and navigate_markers were unregistered until recently,
which hid it. collect_files and import_folder are registered upstream
tools, so they have been failing for everyone.

The literal is now parenthesised, assigned to `result`, and restated as
the final expression, so it parses and is the value the script returns.
(For the mutating tools wrapped in an undo group, that value only reaches
the caller once the wrapper stops ending on app.endUndoGroup() - see
PR ishu86#4.) copy_effects had the same bug and is fixed alongside the other
effects changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
reorder_effects read effect.name AFTER effect.moveTo(). Moving a property
invalidates the reference, so every call threw "ReferenceError: Object is
invalid" - after the move had already happened. The name is now read
first, and both indices are range-checked up front, so an out-of-range
move says so instead of AE's "parent property has only 1 child
properties". Also looks the group up by match name (ADBE Effect Parade)
rather than the English display name.

copy_effects ended with a bare object literal, which parses as a block
and failed with "SyntaxError: Expected: ;". Same root cause and fix as
the marker and asset generators.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Polystar Type is 1 = STAR, 2 = POLYGON. Verified directly: the default
is 1, and Inner Radius is only settable under 1.

The generator had them backwards:
  - shape "star" set type 2 (polygon), then set Inner Radius, which is
    hidden on a polygon -> "property or a parent property is hidden".
  - shape "polygon" set type 1 and silently drew a STAR, with no error.

Also switches add_camera_layer and add_light_layer from English display
names ("Camera Options", "Zoom", "Light Options", "Color", "Intensity")
to match names, as already done for add_shape_layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
create_logo_reveal animates an existing footage item. With neither
logoItemId nor logoItemName, neither branch declared logoItem and the
script died with a bare "ReferenceError: logoItem is undefined". It now
says what is missing before any script is generated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bundled skill documented calls that fail or misbehave. Every claim
below was checked against the tool schemas and generator code, and the
path, effect, blend-mode and example calls were run live in AE 26.5.

Wrong, and failing:
- Nested property paths were written with dots
  ("Effects.Gaussian Blur.Blurriness"). The server splits on "/", so a
  dotted path is looked up as one literal name: "Property not found".
- Shape examples used group "Shape 1". Groups created by these tools
  are "Group 1"; operators sit under "Contents/Group 1/Contents/...".
- apply_expression_template("randomize", ...) - randomize is a text
  animator type, not an expression template.
- blendMode "Screen" - the value is passed through as an AE constant,
  so it must be SCREEN; "Screen" errors with "Value is undefined".

Wrong, and silently ineffective:
- springy was given `frequency`; it takes mass, stiffness and damping.
- The parallax example relied on link_properties' offset, which is
  additive and cannot scale motion. Now uses a multiplier expression.
- modify_layer(motionBlur=...) - no such parameter.
- "Duplicate layer" and time remapping - no such tools. The description
  no longer triggers on time remapping.
- create_logo_reveal style "particle" is listed but has no
  implementation; the skill now says to avoid it.

Also:
- Recommends the camelCase path shortcuts (position, rotation,
  sourceText), which resolve to match names and survive non-English
  installs. Leaves out coneFeather, castsShadows and pointOfInterest,
  whose shortcuts point at the wrong properties.
- Covers the new shape path, operator, mask, 3D and render queue tools,
  including the renderer capability matrix, gradient colours being
  unscriptable, render() blocking, and the output module choosing the
  file type.
- Documents known issues: precompose_layers moves layers then errors,
  and delete_composition leaves its solids behind.
- Tells the model to ask before project-wide operations.
- All examples use named parameters matching the schemas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
create_logo_reveal accepted style "particle" but had no branch for it,
so the logo got no animation and the call still reported success.

ExtendScript has no particle API, but the bundled particle effects are
ordinary effects that can be added by match name. The reveal now:

  1. applies CC Scatterize to the logo and animates Scatter and Right
     Twist down to 0, so the logo assembles out of particles, fading in
     across most of the move;
  2. fires a short CC Particle Systems II burst on its own solid
     ("Logo Particles", ADD blend) as the logo lands.

The values were chosen by rendering frames and looking at them, because
several parameters don't behave as their names suggest:

- Particle Type is a numbered popup whose indices include menu
  separators. The default, 1, draws lines. 5 is Faded Sphere, picked
  from a rendered contact sheet of all 20 entries.
- Radius X/Y and Velocity are relative units, not pixels. Passing the
  logo's pixel size filled the whole frame with particles. On a
  1920-wide layer one radius unit is about layer width / 200, so the
  spawn ellipse is sized from the logo with that ratio.
- Scatterize can't move pixels outside the logo layer's bounds, so a
  large Scatter drew a solid rectangle of noise. It now starts at 150.

Effect parameters are addressed by match name, since effect property
lists are flat and reuse display names. If CC Particle Systems II is
unavailable, the reveal still runs and reports particleBurstAdded:
false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…iles

render_frame reported every render as failed. saveFrameToPng returns
before the PNG is on disk - measured on AE 26.5, it appears about 50 ms
later - and the tool checked for the file immediately, then threw
"Frame render did not produce a file" while the file was still arriving.

It now waits up to 15 s for the file, then until its size has held
steady for three checks, so callers never read a half-written PNG. The
result includes bytes and waitedMs.

It also deletes any existing file at the output path first. Previously
a leftover PNG from an earlier call satisfied the existence check even
when nothing new was written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified against AE 26.5 lights and cameras:

- pointOfInterest mapped to "ADBE Camera Options Group/ADBE Camera
  Point of Interest", which doesn't exist. A camera's Point of Interest
  is its anchor point, relabelled: ADBE Transform Group/ADBE Anchor
  Point. A set_keyframe through the shortcut now keys it and reads back.
- coneFeather mapped to "ADBE Light Cone Feather"; current lights use
  "ADBE Light Cone Feather 2".
- castsShadows mapped to "ADBE Light Casts Shadows"; lights use the
  shared "ADBE Casts Shadows".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Adds a Particles section: which bundled effects provide particles,
  that emitters replace their layer, CC Particle Systems II's relative
  Radius/Velocity units, the Particle Type menu indices (with the
  separators), and HOLD-keyed bursts.
- Notes that effect parameter lists reuse display names, and that a
  parameter's match name works as the last path segment.
- create_logo_reveal "particle" is now implemented, so the warning and
  known-issue entry are gone.
- Lists the camera and light shortcuts now that pointOfInterest,
  coneFeather and castsShadows resolve.
- render_frame now waits for the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Effects render inside their layer's bounds. With CC Scatterize on a
tight text or logo precomp, the scatter filled that rectangle and
stopped at its edges, which read as a hard box. The earlier fix only
lowered Scatter to make the box less visible.

create_logo_reveal "particle" now gives the logo room first:
- if the layer can collapse (precomps, solids, vector layers), it turns
  on Collapse Transformations so effects render in the parent comp's
  space;
- otherwise, for raster footage such as a PNG, it adds Grow Bounds
  ("ADBE GROW BOUNDS", by match name - a Red Giant plugin shares the
  display name) before Scatterize.
With room available, Scatter starts at 450 instead of 150. The result
reports logoBoundsExpandedBy.

canSetCollapseTransformation is the test, not layer type: solids report
true, a PNG layer reports false.

modify_layer gains a collapseTransformation parameter, with a clear
error on layers that can't collapse.

Verified with renders of a text precomp (collapsed) and a PNG (grow
bounds): in both the scatter now extends past the source bounds with
soft edges.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mple

- Effects are clipped to their layer's bounds: use collapseTransformation
  for precomps, solids and vectors, and Grow Bounds (by match name) for
  raster footage.
- apply_effect display names can resolve to a third-party plugin with
  the same name - "Grow Bounds" resolved to Red Giant's on a machine
  with it installed. Check the returned matchName, or pass one.
- Particle previews should go through the render queue: a 4 s particle
  comp rendered in 4 s there, versus minutes frame by frame.
- Example 10: a particle text reveal, the exact calls used to build the
  demo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The skill said every tool call is exactly one undo step named for the
operation. That depends on the double-undo-group fix, which is left to
PR ishu86#4 rather than duplicated here - and ishu86#4's design names the group
after the command ID. Describe only what holds either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gle variants

Measured on AE 26.5 with keyframes at 0, 1 and 2 s, the physics
templates had these problems:

- All four (bounce, overshoot, inertia, springy) keyed off
  key(numKeys), so they only ever affected motion after the final
  keyframe - the deviation after a middle key was exactly 0. They now
  find the most recent key at or before the playhead, so they act after
  every keyframe.
- overshoot did nothing: it measured value - lastKey.value, which is 0
  after the last key, so the layer stopped dead. It is now a damped sine
  seeded with the incoming velocity.
- bounce was a damped-sine overshoot, not a bounce, and wasn't divided by
  the angular frequency, so velocity jumped from 697 to 1291 px/s at the
  keyframe - a visible kink. It is now a gravity/elasticity bounce-back
  (elasticity, gravity, maxBounces) that rebounds off the key value. Old
  amplitude/frequency/decay params are ignored and defaults apply.
- springy divided by the undamped frequency but oscillated at the damped
  one, starting about 13% slow. It now divides by the damped frequency.

Incoming velocity is sampled a tenth of a frame before the key, so it's
the velocity arriving at the key rather than leaving it.

New templates:
- speedControl: accumulates a keyframed "units per second" slider as
  trapezoid areas between keys. `slider * time` runs the property
  backwards as the speed eases off (720 -> 450 -> 0 deg); this holds
  at 720.
- loopingWiggle: crossfades wiggle(t) with wiggle(t - loopTime), so the
  cycle repeats seamlessly.
- wiggleOneAxis: wiggles one axis and leaves the others on their keys.

The techniques follow Dan Ebberts' articles on motionscript.com
(Realistic Bounce and Overshoot; Expression Speed and Frequency
Control). The code is written for this project, not copied.

Template values are now checked before being pasted into expression
source: number params must be finite numbers (a string such as
"3); app.quit(" is rejected), string params are escaped for the quotes
they sit in, and followPath's progress is typed "expression" because it
is inserted as raw code.

add_expression_control now requires controlName. When it was missing
the control was named "undefined", so expressions looking it up by name
never found it.

Verified live: 28/28 checks on Position and Rotation - every template
acts after the middle key, no expression errors, velocity continuous
through the key for overshoot/inertia/springy, bounce rebounds at
elasticity x incoming speed and never passes the key, speedControl gives
the exact integral, loopingWiggle is identical at 0/3/6 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Lists every expression template with its params, grouped. bounce's
  params are now elasticity, gravity and maxBounces.
- Explains that the physics templates act after every keyframe and take
  their size from the incoming speed, so an easy-eased landing key gets
  no effect.
- speedControl, with the add_expression_control call it needs.
- Example 1's bounce now keyframes only the fall and uses gravity 1500:
  verified to give an ~80 px first rebound and seven in all. At 5000 the
  first rebound was only 24 px.
- Example 11: a speed ramp that doesn't rewind, verified to give
  180 / 540 / 720 / 720 degrees at 1-4 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ishu86
ishu86 merged commit 5223c17 into ishu86:main Oct 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants