@ikijs/mcp
v0.26.1
Published
MCP server that lets AI agents build 2D puppet characters: compose AI-drawn part images, check their geometry and auto-rig them into an open .iki model — Iki, the free Live2D alternative.
Maintainers
Readme
@ikijs/mcp
Part of Iki, the open Live2D alternative that AI can build — free and MIT-licensed, with an open
.ikiformat and a Claude Code / Codex plugin that draws and rigs characters.
A stdio MCP server that exposes .iki model tools to AI agents (Claude, LLMs, and any MCP-compatible client): read/validate a model, and auto-rig one from role-named PNG layers.
Tools
| Tool | Description |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| validate_iki | Validate a raw .iki model — accepts an object or a JSON string — fail-fast, one error at a time. |
| describe_iki | Return a summary of a valid model (inline or by modelPath): canvas, parameters, parts, deformers, expressions, motions. |
| list_standard_parameters | List the recommended standard parameter ids (e.g. ParamAngleX, ParamMouthOpenY) with descriptions. |
| auto_rig_from_layers | Auto-rig role-named PNG layers into a renderable .iki written to disk; returns the output path. |
| compose_layers_from_parts | Compose generated part PNGs into canvas-aligned, role-named layers, with the geometry report inline. |
| measure_layers | Re-run that same geometry report over an already-composed layers directory. |
| measure_turn_reference | Measure how far a head turns between a front and a turned image at the same scale, as three ratios. |
| render_poses | Render a rigged .iki into the character loop's pose PNGs through the installed Chrome, Edge or Chromium. |
The model input for validate_iki and describe_iki accepts either a plain JSON object or a JSON string — the server normalises both. describe_iki also takes modelPath instead (exactly one of the two; resolved against the working directory, under it), so a written rig never crosses the context, and returns the patch writer's view of it: per part the rest box in model px, the bustBox in bust-render px and its warps with their stops; per warp2d deformer stopsX and stopsY; and faceWidth (the face part's width, the unit of a patch's by) and bustScale.
auto_rig_from_layers
Turns a set of role-named, full-canvas transparent PNG layers into a renderable, validated .iki model with the textures atlased and embedded as a base64 data:image/png URI. Under quantizeColors a nose layer is the exception: the palette rims a soft-shaded nose's feather, so the nose is atlased alone on a second, lossless page. The written model declares the head motions Nod, Shake and Tilt and up to six default expressions (smile, laugh, angry, sad, surprised, shy; shy needs a blush, angry and sad need brows), each with a description a host picks by; with a blush_L/blush_R layer it also declares ParamCheek, which fades the blush from faint at rest to as drawn.
layers— array of{ path, fileName? }.pathis a PNG file path (resolved against the server's working directory); the role is derived fromfileName ?? basename(path). Required roles:face,eye_L,eye_R, andmouthor the lip setmouth_inner,lip_lower,lip_upper(a mouth that folds open instead of crossfading; a partial lip set, or one mixed withmouth/mouth_open, is refused).mouth_tongue/mouth_teethare optional with the lip set only: drawn under its lips and clipped tomouth_inner, the teeth riding the upper line and the tongue the opening's bottom; without the set they are refused. Optional roles includeiris_L/iris_R,brow_L/brow_R,hair_front/hair_back,lash_L/lash_R,lash_lower_L/lash_lower_R,body,arm_L/arm_R,forearm_pose_L/forearm_pose_R, etc. An arm — the whole arm drawn hanging, its shoulder at the top — needs thebody(it hangs from the body's shoulder) and becomes an elbow cap, an upper arm and a forearm sharing its crop, onParamArmL/R(the shoulder) andParamElbowL/R. A pose forearm — the forearm raised, standing up from its elbow — needs its arm: it hangs from the arm's elbow and is swapped in for the hanging forearm byParamArmPoseL/R, rocked byParamArmPoseAngleL/R, and drawn above the face and the hair. All layers must share the same canvas size (taken from the first layer).outputPath— optional.ikioutput path (resolved against the working directory; the parent directory must already exist, and the path must end in.iki). Defaults toauto-rigged-model.iki.turnTargets— optional head-turn cues to fit the rig to, in the unitsmeasure_turn_referencereports off a front/turned reference pair:eyeShift(plusnoseShiftandmouthShift) is how far that feature slides across the head at full turn, as a fraction of the head's half-width — a magnitude, since the rig turns both ways and the sign is the turn's own business;farEyeRatiois the far/near eye width at full turn over that same ratio at rest;silhouetteRatiothe head's half-width turned over at rest. An omitted field leaves that part of the turn at the profile the rig defaults to. AneyeShiftyou pass is fitted by the turn's amount and clamped when it runs past the room the art leaves the far eye — the face plate's edge, or the bangs' side strand over the far iris — since that room is a fact about these layers, not about the reference. Any other target you do pass that it cannot reach is refused, because that number is a measurement rather than a preference. A layer set without anosestill turns its head, on the profile, but fits no target:turnTargetsis then inert and the result carries noturn.style— optional per-character tuning from the profile the rig defaults to:turn(the whole head turn, the eyes' foreshortening with it, 0..3, 1 = the profile; ignored whenturnTargets.eyeShiftis given),featureLead(how far the features lead the face plate, 0..3),hairFollow(the front hair's share of the face's turn, 0..2, profile 1),outlineFollow(its outer edge's share where it draws the head's outline, 0..2, profile 0: the outline holds),blink(how far the upper lid comes down over the eye's height, 0.1..1, profile 0.6) andsway(the hair sway amplitude, 0..5). A value out of range, or a knob not named here, is refused.patches— optional displacements added to existing keyform stops of the freshly generated rig, each{ target, at, region, by, falloff?, why? }(RigPatchin@ikijs/editor).targetis a feature grid (eyeWarp_L/_R,browWarp_L/_R,noseWarp,mouthWarp;atnamesParamAngleXandParamAngleY, each −30, 0 or 30, never both 0) or a mesh part with warps (face,hair_front, …;atnames one parameter at one of its warp stops).regionis a rectangle in fractions of a role's rest layer box (top-left origin, y down);byandfalloffare in face widths on screen axes (+dx right, +dy down). They are replayed on every rig of these inputs, so they hold through a layout orstyleretune, and they never touch the rest stop or a stop where the rig shuts a part on purpose;bodyWarpand the matrix deformers are not targets. Aim them withdescribe_iki.- Result — on success the result text is the written file path, then one
PATCH <i> <target> at <Param=v …>: <moved> of <of> points (cols a-b, rows c-d), largest move <maxPx> model pxline per patch op ((vertices)in place of the columns and rows for a mesh part), then the rig's fold report lines (asrender_posesprints them), andstructuredContentcarries{ ok: true, path, canvas, partCount, atlasBytes, headHalfWidth?, headHalfWidthApplied, headEdges?, strandEdges?, noseCore?, turn?, patches?, folds }.patchesis what each op moved (index,target,at,moved,of,cols/rowsfor a grid,maxPx) and is present wheneverpatcheswas passed,[]included.atlasBytesis the embedded atlas's data-URI length, summed over both pages when the nose has one of its own.headHalfWidthis the head measured here rather than asked for: the layers' own opaque union at the eye row, under the samealpha >= 128rule and ±10-row bandmeasure_turn_referencespans a render with, so the rig's own rest render measures that span back; it is absent when that union has no span there at all (translucent art painted below alpha 128 — still a valid layer set, just with nothing to measure).headHalfWidthAppliedisfalsewhen the measured value is absent, or no wider than the face plate (a hairless set, or one widest at the plate itself) — the generator will not take such a head, so the plate stands in for it and the rig is still produced either way.headEdgesis a companion to a measured, appliedheadHalfWidth(absent otherwise): every role with an opaque pixel in that same eye-row band, per side, each with its own rest x there — passed straight to the generator's ownoptions.headEdgesso it can tell which part's own deformation actually carries the union's outermost pixel through the turn.strandEdgesis each side's iris and thehair_frontrun it would slide under on the turn, measured whenever the layers includehair_front,iris_Landiris_R: the iris's opaque span and the run's outer and face-side ends as pixel edges on the row holding the iris's centre (a run that reaches the other iris's centre, a fringe spanning the face, hasrunFace: null; a run narrower than half the iris's painted width on that row is hair detail the iris may cross, and is skipped as if clear) — passed straight to the generator's ownoptions.strandEdges, which keeps the far iris from sliding under the bangs any further than it is painted.turnis what the solve settled on —{ radius, holdBase, depths, achieved: { eyeShift, farEyeRatio, silhouetteRatio }, clamped, strandOverlap? }, whereachievedis what the rig does reach andclampednames every target the rig cut down: the turn (eyeShift, passed or the profile's own) where the face plate's edge or the strand cut it short, and a profilenoseShift/mouthShiftpast its room.strandOverlapgives, per side whose far iris the bangs still cover at some far stop, the covered width — how much of the iris's painted row the run covers where that is largest (deg,px, andhhin head half-widths), how much it covers at rest (restPx), and whether the bound held (held:truemeans the iris is kept no deeper under the run than it is painted;falsemeans the art left no room to — always so for a fringe spanning the face — andpxis what stays covered); it is absent when nothing is covered.noseCoreis the nose's dense core in canvas px — its pixels at alpha >= 128, not grown, the box handed to the generator as the nose layer'sdenseCoreand its turn landmark — absent without anoselayer, when it has no pixel at that threshold (painted wholly translucent), or when that core is a speck of the nose's crop (under a quarter of its width or height, such as a lone nostril mark), and in both of the last two cases the crop stands in for it.turnis absent without anoselayer.foldsis the written model's fold report, the engine'scheckFoldsover its own pose set ({ poses, thresholds, targets, offenders }, described underrender_poses); it never refuses a rig — a model that folds is still written and returned — and each fold names its pose by its parameter settings, which a host reproduces by setting those parameters on a player (IkiPlayer.setParameter). The (potentially multi-MB) model is written to disk, not inlined. Invalid input (apatchesop the rig refuses —INVALID: patches[i]…, naming the stops, targets or roles there are; unknown/missing role, an arm without abody, empty layer, mismatched sizes, bad path, an atlas page over 4096 px on a side, or an atlas over 12 MiB; art the rig cannot build on, such as a body's hips with no room under its pivots for a row line or for the body's motion, or an arm too short to hang an elbow under its shoulder; aturnTargetsfield that is unknown or that this layer set cannot reach — any passed field other than aneyeShiftpast the art's room; astyleknob out of range or unknown; the message names the field and the attainable range) returns{ ok: false, error }with aINVALID: …text — not a protocol error. An argument the tool's input schema refuses — aturnTargetsorstylethat is not an object, or one of their named fields that is not a number — never reaches the tool: the MCP SDK answers with anisErrorresult whose text isMCP error -32602: Input validation error: …, and nostructuredContent.
The decode/atlas pipeline runs in Node via sharp (a native dependency confined to this package); it mirrors the browser editor app's import flow and reuses the pure @ikijs/editor model + atlas math, so both paths produce the same rig. The fold check runs the workspace @ikijs/engine, inlined into this package's Node build (as it already is into the render_poses page script), while @ikijs/format stays an external dependency.
compose_layers_from_parts
Composes a directory of AI-generated part PNGs into canvas-aligned, role-named layer PNGs ready for auto_rig_from_layers, on a canvas 1100 px wide and canvasHeight tall (1100 by default) with a built-in default layout tuned for the character-generation skill's standard front-facing framing. It ports the composer that shipped as a script (compose.cjs) in the Claude Code plugin.
partsDir— directory of the source part PNGs (resolved against the server's working directory). Required sources:face.png,eyewhite.png(split into the sclera, the upper lash and the lower lid —lash_lower_L/lash_lower_R, the dark line and lashes of the eye's lower part, which draw over the iris and are placed on the sclera's frame by theeye_*layout, so they have no layout key of their own; an eyewhite with no dark lower lid writes none and lists them inskipped. A light mark drawn detached above the eye white, such as a double-eyelid crease, is dropped from the sclera — the iris's clip — while dark strokes there stay lash ink. So is the lid's skin drawn attached above the white, a warm light band over the lash, in the columns that have a lash there — a band over a lash-free column, such as the tear duct, stays, as the white's own warm shading would — while the eye's frame stays the drawing's),iris.png,brow.png,hair_front.png, and a mouth by one of two routes:mouth.png(a closed drawing, with an optionalmouth_open.pngit cross-fades to), or the lip set —mouth_keyed.png(the open mouth, its inside painted ONE flat #00FF00) +mouth_interior.png(the cavity, teeth and tongue alone), which the composer keys and splits intomouth_inner/lip_lower/lip_upperon one frame, soauto_rig_from_layersfolds the mouth open like the eyelid. The interior's light paint (luma 100 and over: the teeth, the tongue) is written asmouth_teethandmouth_tongueon the same frame, split at each column's middle, riding the upper line and the opening's bottom and clipped to the cavity;mouth_innertakes the cavity's fill under it, its edge softened by the key's own coverage. An interior with no light paint writes neither and lists them inskipped. The key reads each antialiased pixel against its own ramp to the art's green, the interior is trimmed of the lip rim a generator draws round it, and the split is cut at the composed size so every boundary meets the rig's per-column contract by construction. The lip set is opt-in: with neither file the legacy route runs; one without the other, or either besidemouth.png/mouth_open.png, is refused naming the file to draw or remove. With the set,mouthandmouth_openare neither written nor listed inskipped. Optional:hair_back.png,body.png,mouth_open.png(legacy route),blush.png,nose.png— a parts dir without them still composes, minus those roles. Oneblush.pngmakes both cheeks' blush:blush_Rtakes it as drawn andblush_Lmirrored. It is decoration, so the report does not flag a set without it. Onearm.png— the arm on the screen left, drawn hanging with its shoulder at the top — makes both arms of a full body:arm_Rtakes it as drawn andarm_Lmirrored, and each is hung on the body's shoulder (seelayout). The arms are opt-in: a parts dir withoutarm.pngcomposes as a bust does, andskippednever lists them. Anarm.pngneeds abody.png. Oneforearm_pose.png— the arm's forearm raised, on the screen left, standing up from its elbow with the hand at the top — makes both pose forearms (forearm_pose_Ras drawn,forearm_pose_Lmirrored); it is opt-in too and needs anarm.png. With one the compose also writespreview-pose.png, the switch at 1 as the rig draws it (each arm with a pose forearm cropped to the rows above its elbow, the pose forearms over everything);preview.pngstays the rest pose, where a pose forearm is invisible. Without aforearm_pose.png,preview-pose.pngis removed. The nose is what the auto-rig leads the head turn with, and a face supplied alongside it must be drawn without one, or the painted nose stays on the face plate while the real one moves. Each part but the eye pair is cropped to its drawing before it is sized and placed. A faint speck detached from the drawing, one that never reaches alpha 128 and covers under 1 % of the part's pixels (residue a background removal leaves), is cut out with the margin it widened, so it cannot pull the part off its centre. A detached stroke that reaches alpha 128 is kept.outDir— an existing directory (resolved against the working directory, confined to it exactly likeauto_rig_from_layers'soutputPath; the tool never creates directories) to write the role layer PNGs and a flattenedpreview.png(and, with aforearm_pose.png,preview-pose.png) into. Reusing a directory across runs is intentional — a role this run does not write (one it skips, e.g. composing a head-only set that omitsbody, or an arm or pose forearm oncearm.pngorforearm_pose.pngis gone,preview-pose.pngincluded) has its stale<role>.pngfrom an earlier run deleted, so the directory always holds exactly this run's roles.layout— optional per-role override merged over the built-in defaults, e.g.{ "eye_L": { "cx": 660 } }, so a character can be retuned by re-running compose rather than editing a script. Each ofcx,cy,w,his optional;wmust be an integer in 1..1100 (the canvas width) andhin 1..canvasHeight(the canvas height). The defaults of the face's features — blush, nose, mouth,mouth_open,mouth_inner, eyes, irises, lashes, brows — are proportions of the face: an override offace.cx,face.cyorface.wcarries them with it (scaled by the width;face.hstretches the face alone), while a feature's own override names canvas px. Omittinghkeeps the part's own aspect ratio, which is what every role wants until one does not: a generated eyewhite often comes back flatter than its reference, and stretching a flat white lens costs nothing where regenerating it is billed and unreliable. The nose is sized and placed by its dense core — its pixels at alpha ≥ 128, the drawing a viewer reads inside a soft nose's feather — rather than its whole part, which for a shaded-bump nose is mostly feather:w(default 20) andhsize the core,cx(default 550) centres it, and a setcycentres its rows. By default itscyis unset, and the core's bottom row (the tip) lands 0.66 of the way from the eye row down to the mouth's, so the nose follows a retuned eye or mouth row. A nose with no pixel at alpha ≥ 128 is sized and placed whole, and so is one whose core is a speck of its trimmed part — under a quarter of its width or height, such as a lone nostril mark or highlight, which sized towwould blow the whole nose up — and the report warns of the speck.mouth_inneris the lip set's one frame (mouth_open's place by default):lip_lower,lip_upper,mouth_tongueandmouth_teethhave no key and land on it, so a retune moves them together, and with the set the nose's tip rule readslayout.mouth_inner.cy. Sethon a sclera and its lash together — they share one frame, and the blink fold tears if they diverge, so a pair set to land on different frames is rejected.arm_Landarm_Rhave nocx/cy/wdefaults; each is derived from the placed body. An unsetwis 0.4 of the body's width. An unsetcxorcyhangs the arm's shoulder pivot — the oneauto_rig_from_layersturns it about, read off the resized arm by the rig's own rule — on the body box's shoulder corner on that axis: its left edge forarm_R, its right edge forarm_L, 0.12 of its height under its top. On a torso widest at its shoulders, the shoulder cap's centre then sits on the torso's edge. A set one centres the arm as for every other role. Both values are provisional.forearm_pose_Landforearm_pose_Rhave nocx/cy/wdefaults either: an unsetwscales the forearm so its elbow end (the shoulder rule run from its bottom, on the trimmed source) is as wide as the placed arm's elbow run, and an unsetcxorcypins the elbow end's pivot — measured on the resized part — on the arm's placed elbow, the pivotauto_rig_from_layershangs it from.mirrorParts— optional list of part files to flip left-right as they are read, e.g.["eyewhite.png"]. Which way a part faces is fixed by the layout, and "left eye" cannot pin it down — it reads as the viewer's left or the character's — so in image terms:eyewhite.pngis read as the eye on the screen left, lash wing (outer corner) at the image's left end and lash-free tear duct at its right;brow.pngas the brow on the screen right, thick head at the image's left end and tail at its right;blush.pngas the blush on the screen left, outer end (toward the face's edge) at the image's left end and inner end at its right;arm.pngas the arm on the screen left (the character's right), hanging, its shoulder at the image's top;forearm_pose.pngas the raised forearm on the screen left, standing up from its elbow, the hand at the image's top and the thumb toward the body at its right. A part drawn the other way round is fixed here for free instead of regenerated. It flips the source, so every role cut from it flips together and an eye's sclera and lash stay in one frame.canvasHeight— optional height of the canvas in px: an even integer in 1100..4096, default 1100, a square bust canvas — and at 1100 every output is byte-identical to leaving it out. The width stays 1100. The canvas grows downward: every default and every override keeps its canvas px from the top-left, so the head lands where it does on a bust and a full body gets room below. An odd height is refused, since model space is centred on the canvas and it would put the head on a half pixel. It is checked before any part decodes.- Result — on success the text is the written layer paths (one per line) followed by the same geometry report
measure_layersreturns — except that when the composer found the core of the nose's source part a speck, its nose check reports that verdict instead of what the composed file shows, so a speck that resampling erased from the layer is warned about here and not bymeasure_layers.structuredContentis{ ok: true, outDir, layers, skipped, preview, previewPose?, measure }(previewPoseonly with aforearm_pose.png). With the lip setpreview.pngshows the mouth closed, as the rig draws the rest pose (the tongue and the teeth left out with the interior), andskippedlistsmouth_tongue/mouth_teethwhen the interior has no light paint. Invalid input (a missing required part, acanvasHeightthat is odd or outside 1100..4096, an unknownlayoutrole, an out-of-rangeworh, a lash placed on a different frame from its sclera's, anarm.pngwithout abody.png, aforearm_pose.pngwithout anarm.png, a lip set with one source missing or beside the legacy mouth, a key that fails (no green, green outside the opening, an outline not closed round it, more than one opening, no skin under it, an interior that is only lips or drawn opaque on a non-white or noisy ground), an arm too short for the rig to hang an elbow under its shoulder, amirrorPartsentry that is not a part file, a part that lands entirely off-canvas, a non-existentoutDir) returns{ ok: false, error }withINVALID: …text.
measure_layers
Reports the same read-only geometry checks compose_layers_from_parts returns inline (iris size/offset relative to the sclera, eye aspect, an eye drawn facing the other way, cropped or flat-cut edges, a nose whose dense core is a speck of its crop, missing optional roles, and for a full body: a body whose feet the bottom of a canvas taller than wide cuts off, an arm without a body, an arm whose shoulder cap — the one auto_rig_from_layers turns it about, read by the rig's own rule — does not reach half its radius under the torso's edge on its shoulder pivot's row, a pose forearm without its arm or whose elbow end is more than 2 px off the arm's elbow, more than 10 % off its elbow run in width, or more than 15 % off its hand's reach in length, and for the lip set: a partial set or one beside mouth / mouth_open, a column of the opening where the interior does not reach the line or the skin does not reach the interior, an opening under 0.3 of the upper lip's width or under 8 rows tall, a line under 2 rows, key green left, an interior with transparent holes, and a mouth_tongue / mouth_teeth with no lip set, on another canvas, or whose box leaves mouth_inner's, where the cavity clips it away), over an already-composed layers directory — for re-checking a layers dir without recomposing it. Its nose check reads the composed file against its crop, which is what auto_rig_from_layers acts on: a speck core there is one the rig does not turn or tilt the nose about. The composer's inline report does the same unless it found the source part's core a speck, which it reports instead, so a speck that resampling erased from the layer shows there and not here.
layersDir— directory of role-named layer PNGs (resolved against the working directory). Everypreview*.pngin it is skipped.- Result — the text is the report: a per-layer size/bbox-centre/mass-centre/margins table, then, with a lip set, one
lips:line — the opening's width, its height at the centre, the line's rows and theMouthOpenbelow which no slit shows — then the checks that fired.structuredContentis{ ok: true, layersDir, layers, empty, warnings, lips?, passed }. A missing or non-directorylayersDirreturns{ ok: false, error }withINVALID: …text.
measure_turn_reference
Measures how far a head turns between two images of the same character — one facing front, one turned — and reports it as three ratios, so a rigged turn can be compared against a reference turn (or against an earlier rig) without registering the two images. Every number is a front→turned change divided by something measured in the same image, which cancels crop and position — not scale: the pair must already share framing and head size. A yaw never changes iris height, so that is what is checked — a pair whose mean iris height differs by more than 10% is refused rather than measured as if it were a turn.
front,turned— PNG file paths (resolved against the server's working directory). Engine renders (transparent backdrop) and opaque reference art both work; see the two foreground modes below.iris— optional colour-window override merged over the violet default (hueMin230,hueMax300 degrees,satMin0.22). The iris is found as a saturated blob inside that hue window, closed and paired on one row, so a character whose eyes are another colour needs its own window — otherwise the tool returnsno iris pair found …naming the file and how many candidate blobs it had.debugDir— optional existing directory (confined to the working directory, like every other write) to drop afront-<name>.debug.pngand aturned-<name>.debug.pnginto (role-prefixed, so two inputs that share a basename do not overwrite each other), with the iris boxes, the head edges, the head centre and the eye-pair centre drawn on them. The measurement is only believable once those boxes are seen sitting on the irises.- Result —
structuredContentis{ ok: true, front, turned, farEyeRatio, eyeShift, silhouetteRatio, turnSign, debug? }, withfront/turnedcarrying the raw per-image numbers (iris widths and centres, eye row, head span, which foreground mode was used). A missing file, a missingdebugDir, an image with no iris pair, or a pair whose mean iris heights differ by more than 10% (not at the same scale) returns{ ok: false, error }withINVALID: …text.
The three ratios:
| Ratio | What it is |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| farEyeRatio | Turned far/near iris width, divided by that same far/near ratio at rest — so a resting asymmetry in the art divides out. |
| eyeShift | How far the eye pair slides across the head, in units of the front head half-width (hh). Negative = toward the image's left. |
| silhouetteRatio | Head half-width, turned over front. |
turnSign is -1 when the pair moved toward the image's left and +1 when it moved right; the "far" eye is the one on the side it moved toward.
The head span is the silhouette at the eye row, over a ±10-row band — fixed rather than scaled to the image, so compare renders at like resolutions. The foreground it spans is found two ways, chosen per image:
- alpha — the file has an alpha channel with transparent pixels (an engine render): foreground is
alpha >= 128and nothing else, so the character's near-black hair ink and pale highlights all count. - keyed — a fully opaque image (a reference): the flat lavender-grey backdrop (hue 220–260, low saturation, bright) and near-black frame borders are keyed out by colour instead.
The two rules do not agree on the same picture — a near-black outline is silhouette under alpha and background under keyed — so compare renders with renders and references with references.
render_poses
Renders a rigged .iki into the pose PNGs the character loop reviews, with the real engine, so the render step is one tool call. The engine runs in the Chrome, Edge or Chromium already installed on the machine, headless and driven over DevTools by the server itself: no Playwright and no browser download. Set CHROME_PATH to an executable to pick a browser; without it the usual install locations are searched, and with none found the tool returns INVALID: … saying so, and a CHROME_PATH that is not an executable file is refused the same way. A browser that is found but fails to start is a tool error. It needs Node 22 or newer.
modelPath— the rigged.iki(resolved against the working directory; must sit under it).outDir— an existing directory under the working directory. One<pose>.pngper pose is written into it, replacing a same-named file in place.poses— optional caller-named poses rendered instead of the standard set, each{ name, values, crop? }:namebecomes<outDir>/<name>.png(letters, digits,_,.,-; unique ignoring case),valuesare parameter settings (the rest at their defaults), andcrop{ x, y, w, h }is a rectangle in model px from the top-left — the frame ofdescribe_iki's boxes and theFOLDlines — clamped to the canvas and drawn 720 px wide (an empty list, a bad or repeated name, a non-finite value, or a crop that misses the canvas or would draw under 1 or over 8192 px tall isINVALID: poses…; a parameter id the model lacks is ignored). Each pose is also added to the fold sweep, after the engine's own poses.- What is written — the model decides the set. A bust gets 560 × 560 shots:
rest,turn-m30,turn-p30,turn-15,nod-15,blink,blink-half,gaze,combined(ParamAngleX/Y/Zall at 30),stress(ParamAngleX/Y/Zall at −30,ParamMouthOpenY1,ParamEyeLOpenandParamEyeROpen0.5,ParamBrowLY−1,ParamBrowRY1,ParamBrowLAngle1,ParamBrowRAngle−1, and each ofParamBodyAngleX/Y/Zthe model declares at −10: moves that each look fine alone, stacked). A model with amouthormouth_inneradds 720 × 480 mouth crops (mouth-open-*-form-*,mouth-laugh,mouth-surprised,mouth-turn-p30-half). A full body (canvas taller than wide) shoots the same squares from the model's top (the head), centred horizontally, and adds whole-canvasfull-*shots (body, arm and elbow poses) at 1400 px wide; one with a pose forearm (ParamArmPoseR) adds thefull-pose-*shots. - Result — the text is the written paths, one per line, then the model's fold report.
failedTextureslists the atlas pages the engine could not load; their parts are missing from the renders.structuredContentis{ ok: true, outDir, canvas, fullBody, mouth?, failedTextures, poses: [{ name, path }], modelCanvas, folds }. A missing model, anoutDirthat is missing or outside the working directory, or a file that is not a valid model returns{ ok: false, error }withINVALID: …text. - Folds —
foldsis the engine'scheckFoldsover its own pose set (foldPoses: rest, each parameter alone at its min and max, and the corners of the head and body angles crossed with the eyes, the mouth, the arms and the elbows), not over the rendered shots:{ poses, thresholds: { flip: 0, collapse: 0.1 }, targets, offenders }, one target per warp deformer (its grid cells) and per mesh part (its triangles), each{ target: { kind, id }, kind, worst?, expected? }:worstandexpectedare the least ratio of an area at a pose to the same at rest over the poses that are not, and that are, a by-design closure (below), each with its cell or triangle, its pose and its rest point;kind(flip,collapseorok) is read offworstalone, andoffendersare the targets that are notok. The text prints oneFOLDline per offender, worst first —flip(ratio ≤ 0) orcollapse(under 0.1),deformerorpart, its id,cell (col,row)ortriangle i, the ratio (two significant figures under 0.01, so a flip never reads-0.00), the pose in brackets as its sortedid=valuesettings (restfor none), and where it sits at rest in model space and in model px from the top-left ofmodelCanvas(the layer PNGs' own pixels, not a render's) — orFOLDS: none over N poses. Then, when one folds, aFOLDS expected:line names the closures the auto-rig draws on purpose, each by part id, ratio and the parameter that shuts it:eye_Landlash_lower_LwhileParamEyeLOpenis 0,eye_Randlash_lower_RwhileParamEyeROpenis 0, and the lip set (mouth_inner,mouth_tongue,mouth_teeth,lip_lower,lip_upper) whileParamMouthOpenYis 0, read as each pose's effective value, its default included. The sweep covers the engine's pose set plus the caller'sposes; a caller's pose is named in theFOLDlines by its sortedid=valuesettings (restwhen empty), never by its PNG name, so a host reproduces any fold by setting those parameters on a player. Report-only: a fold never fails the render.
Usage
Run directly with npx (no install needed):
npx -y @ikijs/mcpOr install globally and use the iki-mcp bin:
npm install -g @ikijs/mcp
iki-mcpClaude Desktop / Claude Code / Codex config
Add to your MCP server config:
{ "mcpServers": { "iki": { "command": "npx", "args": ["-y", "@ikijs/mcp"] } } }For Claude Desktop this goes in claude_desktop_config.json; for Claude Code it goes in .claude/mcp.json (or the equivalent per-project config). For Codex, register it from the shell:
codex mcp add iki -- npx -y @ikijs/mcpScope note
Current tools cover read/validate, auto-rigging a model from PNG layers (auto_rig_from_layers), composing/measuring the role layers a generated character needs (compose_layers_from_parts, measure_layers), measuring a rendered head turn against a reference one (measure_turn_reference), and rendering a rigged model's poses (render_poses). PSD input and granular model-mutation primitives (add part, bind parameter, export) are deferred to future slices.
License
MIT © Zeikar
