Skip to main content
Glama

ps_apply_brush_stroke

Destructive

Paint strokes along a defined path or resolved placement with Photoshop brush tools for precise retouching, cloning, healing, dodging, burning, and freehand painting while preserving original layer.

Instructions

Paint along a path with one of PS's 16 brush-family tools — supply the path EITHER as an anchor-relational placement (preferred: a path relation → the stroke traces a resolved, gate-verified curve along a traced edge / landmark / between two anchors, no pixel-guessing) OR as an explicit path list of anchor points — the retouch tools (healing_brush, clone_stamp, burn, dodge, blur, sharpen, smudge), the paint family (brush, pencil, eraser), and the specialty tools (pattern_stamp, art_history_brush, history_brush, color_replacement, background_eraser, sponge). The path parameter takes a list of anchor points with optional bezier handles, so the same tool handles straight-line strokes, freeform curves, and closed shapes — all by varying the path geometry. Reach for this when: (a) cloning out a distraction along a specific shape (clone_stamp with a source_point + a path tracing the unwanted edge); (b) healing a scratch or seam (healing_brush with source_point); (c) dodging / burning to redirect tonal balance along a contour; (d) painting a freehand line into the canvas (brush + foreground_color). Active layer must be a normal pixel layer (background auto-promotes); rasterize adjustment/shape/text/smart-object layers first. Auto-duplicates — the original is preserved and a "Brush Stroke ()" copy receives the paint. Brush dynamics: hardness, opacity, and flow are independently settable via hardness_pct / opacity_pct / flow_pct (the live tool options are mutated before stroking and restored to the user's prior state in finally). Sampled brushes (custom shape-stamp presets) silently ignore hardness/diameter mutations — vary their character via brush_preset instead.

Tool call template

Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.

{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "arguments": {
      "tool": "<tool>",
      "brush_size": "<brush_size>"
    },
    "name": "ps_apply_brush_stroke"
  }
}

Input Schema

NameRequiredDescriptionDefault
pathNoOrdered list of anchor points the stroke traces. **Minimum 2 anchors.** Each anchor is `{x, y}` for a sharp corner OR `{x, y, in: [hx, hy], out: [hx, hy]}` for a smooth bezier point. The `in` and `out` handles MUST be positioned **tangent** to the curve at the anchor — `in` placed in the direction of the PREVIOUS anchor in the array, `out` placed in the direction of the NEXT anchor. Handles placed RADIALLY (toward/away from the shape's center) produce loops + concave curves instead of smooth convex ones. Mix corner + smooth points freely. Coordinates are document pixels; (0, 0) is top-left. Partial handles (only `in` OR only `out`) degrade to a sharp corner. **Recipes for common natural-stroke shapes** (compute these client-side and emit the resulting `[{x, y}, ...]` array): (1) **Hand-drawn straight line A→B with sketchy feel**: sample 8-30 evenly-spaced corner anchors along the line, then perturb each interior anchor by ±2-5px on the perpendicular axis — OR pass clean anchors and use `jitter_px` to apply the perturbation server-side (preferred — cheaper, deterministic). (2) **Sine wave A→B, amplitude a, periods n**: for i in 0..N, x_i = A.x + (i/N)*(B.x - A.x), y_i = A.y + a * sin((i/N) * 2π * n). Use 20-40 anchors for a smooth wave. (3) **Parabolic arc A→B peaking height h above midline**: for i in 0..N, t = i/N, x_i = lerp(A.x, B.x, t), y_i = lerp(A.y, B.y, t) - 4*h*t*(1-t). (4) **Canonical clockwise circle of radius r around (cx, cy)** with k = r * 0.5523: TOP `{x: cx, y: cy-r, in: [cx-k, cy-r], out: [cx+k, cy-r]}`, RIGHT `{x: cx+r, y: cy, in: [cx+r, cy-k], out: [cx+r, cy+k]}`, BOTTOM `{x: cx, y: cy+r, in: [cx+k, cy+r], out: [cx-k, cy+r]}`, LEFT `{x: cx-r, y: cy, in: [cx-r, cy+k], out: [cx-r, cy-k]}` — close with `closed: true`. (5) **Many short overlapping strokes for ink-on-paper texture**: chain multiple `apply_brush_stroke` calls along the same trajectory with small position offsets and varying `brush_size`; reads more natural than one long stroke.
toolYesWhich brush-family tool to dispatch. Headline retouch options: `healing_brush` and `clone_stamp` (both REQUIRE `source_point` — set the sample location, then stroke the path); `burn` darkens; `dodge` lightens; `blur` smooths; `sharpen` enhances local contrast; `smudge` pushes pixels in the stroke direction. Paint family: `brush` (the standard paintbrush — honors `foreground_color`), `pencil` (hard-edge), `eraser`. Specialty: `pattern_stamp`, `art_history_brush`, `history_brush`, `color_replacement`, `background_eraser`, `sponge`.
closedNoWhen true, the path is closed (last anchor connects back to first) so the stroke forms a loop. Default false.
flow_pctNoOptional brush flow override (percent, 0-100). Routed through `currentToolOptions.flow` (top-level integer key). Flow controls how much paint each stroke step deposits — for paint-family tools (`brush`, `pencil`, `pattern_stamp`), low flow means the stroke needs multiple overlapping passes to reach full opacity; high flow saturates immediately. Restored to the user's original value in `finally`.
jitter_pxNoServer-side hand-drawn perturbation. When > 0, the handler shifts every INTERIOR anchor (not the first, not the last — those stay exactly where you placed them) by a deterministic pseudo-random offset in `[-jitter_px, +jitter_px]` on each axis BEFORE stroking. Bezier handles ride along with their anchor so curve tangents are preserved. Same inputs produce the same emitted shape every call. **Reach for this when** you want a clean intent (mathematically perfect line, sine wave, arc) to read as hand-drawn ink without writing the jitter math yourself. Typical values: 2-5 for subtle ink texture, 5-10 for visibly sketchy, 10-20 for cartoonish/scratchy. Default 0 (no jitter).
placementNoANCHOR-RELATIONAL stroke path (preferred over supplying pixels): a PATH relation — `along` a traced edge or a face-mesh landmark curve, where available, `offset-curve`, or `segment` between two anchors — so the stroke traces the resolved, gate-verified curve (the FULL polyline, not just endpoints: paint along the jaw / horizon / under-eye). Strokes ONLY if the gate PASSES. When set, `path` is ignored; tool/brush_size/source_point/colors/dynamics/jitter still apply. See ps_resolve_placement for the anchors + relation vocabulary.
brush_sizeYesBrush tip diameter in pixels. Typical retouch values: 12-30 for fine work, 50-100 for filling, 150+ for broad strokes. Verified scriptable via `setd Brsh.Trgt masterDiameter` in the 2026-06-09 spike.
opacity_pctNoOptional brush opacity override (percent, 0-100). Routed through `currentToolOptions.opacity` (a top-level integer key on the tool options descriptor, not nested inside the brush sub-descriptor). Applies to the WHOLE stroke uniformly — varying-opacity strokes require chaining multiple `apply_brush_stroke` calls. Restored to the user's original value in `finally`.
brush_presetNoOptional brush-preset name to select before stroking (e.g. "Soft Round", "Hard Round", or any custom preset the user has loaded). The preset's saved hardness, opacity, flow, and shape dynamics load with the slct dispatch — set the preset for a coherent character (soft vs hard, calligraphic vs round), then optionally override individual values via `hardness_pct` / `opacity_pct` / `flow_pct`. If the named preset is not installed, the tool falls back to "Soft Round" then "Hard Round" (both ship on every install) and reports which one landed via `preset_applied` in the result.
hardness_pctNoOptional brush hardness override (percent, 0-100). When set, mutates the live tool's hardness via the `currentToolOptions.brush.hardness` get-mutate-set pattern AFTER any brush_preset has loaded — so the preset's hardness is what you start from and this overrides it. **Computed brushes only**: Soft Round / Hard Round / Calligraphic / etc. accept the mutation; **sampled brushes** (custom shape-stamp presets loaded from .abr files) silently ignore it because their shape isn't parametric. The user's original tool options are restored in `finally` so this doesn't leak to their next non-LLM brush action.
source_pointNoSample point for `clone_stamp` / `healing_brush` — the pixel location PS samples from while the stroke progresses. REQUIRED for those two tools (unless `source_placement` names it instead). Ignored for all others. `layer_name` defaults to the active layer at call time when omitted.
foreground_colorNoRGB foreground color for paint-family tools (`brush`, `pencil`). The retouch family (`burn`/`dodge`/`blur`/`sharpen`/`smudge`/`clone_stamp`/`healing_brush`) ignores foreground color — setting it on those tools is harmless but pointless. The user's previous foreground color is restored after the stroke completes.
source_placementNoGrounded alternative to `source_point` for `clone_stamp` / `healing_brush`: NAME the sample location (resolves to a POINT via the grounding resolver + objective gate — e.g. an `extremum` for the cleanest/smoothest nearby skin, a `grid` intersection, or a landmark point) instead of guessing pixels. Resolves to a POINT relation (centroid / midpoint / offset / extremum / grid / landmark point); the resolved point supplies `source_point` and WINS over an explicit `source_point`. Strokes only if the source gate PASSES.
apply_to_active_layerNoIf false (default), the stroke is applied to a duplicate of the active layer named "<OpName> (<Original Name>)" — the original is preserved and the LLM can undo simply by deleting the copy. If true, the stroke bakes directly into the active layer (the historical destructive behavior).

Output Schema

NameRequiredDescriptionDefault
toolNo
closedNo
anchorsNo
contextNo
strokedNo
placementNoPresent when the stroke path came from anchor-relational placement: the resolved curve + gate verdict.
tool_typeNo
brush_sizeNo
flow_appliedNo
path_removedNo
size_appliedNo
preset_appliedNoName of the brush preset that actually landed — equal to `brush_preset` if installed, "Soft Round" / "Hard Round" if a fallback fired, null when no preset was requested.
opacity_appliedNo
stroke_envelopeNoThe doc-pixel bbox the stroke should occupy (path bbox + brush radius) — the objective target to verify stroke occupancy against.
target_was_copyNo
clone_source_setNo
hardness_appliedNo
source_placementNoPresent when the clone/heal sample point came from a source_placement: the resolved point + gate verdict.
target_layer_nameNo
background_promotedNo
original_layer_nameNo

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed1 schema field changedv1.9.6
    • changedInput schema / properties / placement / description
      Previous value: -"ANCHOR-RELATIONAL stroke path (preferred over supplying pixels): a PATH relation — `along` a traced edge or a Pro face-mesh landmark curve, `offset-curve`, or `segment` between two anchors — so the stroke traces the resolved, gate-verified curve (the FULL polyline, not just endpoints: paint along the jaw / horizon / under-eye). Strokes ONLY if the gate PASSES. When set, `path` is ignored; tool/brush_size/source_point/colors/dynamics/jitter still apply. See ps_resolve_placement for the anchors + relation vocabulary."New value: +"ANCHOR-RELATIONAL stroke path (preferred over supplying pixels): a PATH relation — `along` a traced edge or a face-mesh landmark curve, where available, `offset-curve`, or `segment` between two anchors — so the stroke traces the resolved, gate-verified curve (the FULL polyline, not just endpoints: paint along the jaw / horizon / under-eye). Strokes ONLY if the gate PASSES. When set, `path` is ignored; tool/brush_size/source_point/colors/dynamics/jitter still apply. See ps_resolve_placement for the anchors + relation vocabulary."
  2. Changed4 schema fields changedv1.3.0
    • changedInput schema / properties / placement / properties / anchors / description
      Previous value: -"Named anchors, same vocabulary as ps_resolve_placement (face/object/grid/extremum/corner/edge/landmark)."New value: +"Named anchors, same vocabulary as the placement-resolver tool, when this build has one (face/object/grid/extremum/corner/edge/landmark)."
    • changedInput schema / properties / placement / properties / relation / description
      Previous value: -"The relation, same vocabulary as ps_resolve_placement."New value: +"The relation, same vocabulary as the placement-resolver tool, when this build has one."
    • changedInput schema / properties / source_placement / properties / anchors / description
      Previous value: -"Named anchors, same vocabulary as ps_resolve_placement (face/object/grid/extremum/corner/edge/landmark)."New value: +"Named anchors, same vocabulary as the placement-resolver tool, when this build has one (face/object/grid/extremum/corner/edge/landmark)."
    • changedInput schema / properties / source_placement / properties / relation / description
      Previous value: -"The relation, same vocabulary as ps_resolve_placement."New value: +"The relation, same vocabulary as the placement-resolver tool, when this build has one."
  3. First observedv1.0.2

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare destructiveHint=true and idempotentHint=false; the description goes far beyond by disclosing that strokes auto-duplicate to a "Brush Stroke (<name>)" copy, that `apply_to_active_layer=true` is the destructive historical path, that layer type must be a normal pixel layer, that tool options are mutated and restored in `finally`, and that sampled brushes silently ignore hardness/diameter. That is exactly the extra context annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The critical routing info is front-loaded, but the first sentence is a long run-on parenthetical that re-lists all 16 tool names already present in the `tool` enum, which is redundant against structured data. The stroke-recipe material is genuinely useful but is duplicated conceptually by the `path` parameter's own recipe block.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 14-parameter mutation tool with nested objects, the description covers the things an agent would otherwise get wrong: layer eligibility, non-destructive duplication, gate-gated placement resolution, option restoration, and preset/sampled-brush caveats. An output schema exists, so return-value documentation is correctly omitted.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema coverage the baseline is 3, but the description adds real precedence semantics not present in the schema: `placement` causes `path` to be ignored, `source_placement` WINS over an explicit `source_point`, and `brush_preset` loads before `hardness_pct`/`opacity_pct`/`flow_pct` override it. The tool enum is restated from the schema, which adds no new meaning, keeping it below a 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence gives a specific verb+resource (paint along a path with one of PS's 16 brush-family tools) and immediately enumerates the three families covered, so the agent knows exactly which dispatch surface this owns versus siblings like ps_retouch, ps_filter, or ps_fill_layer.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is an explicit "Reach for this when" block with four concrete scenarios (clone out a distraction, heal a seam, dodge/burn along a contour, freehand paint), plus an explicit routing rule: prefer anchor-relational `placement` over pixel `path`, and the layer-type prerequisite is stated. Alternatives are named rather than implied.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.