Writing a plugin
The kinetograph field in package.json, a props schema, a Studio panel, MCP tools and a SKILL.md.
A package with a kinetograph field in its package.json is a plugin. Every key is optional, and paths are relative to the package root.
{
"name": "my-plugin",
"kinetograph": {
"skill": "./SKILL.md",
"panel": "./studio/panel.ts",
"tools": "./tools.ts",
"title": "My plugin",
"description": "One line for its card in Studio.",
"icon": "./icon.svg",
"components": ["Genome", "GenomeRing"]
}
}skill,panelandtoolspoint at the skill file, the panel module and the MCP tools module.titleanddescriptionname the plugin on its Studio card. Without atitle, Studio shows the package name without its scope.iconis an SVG that Studio draws in the text colour, so usecurrentColor.componentslists the React components that draw layers. Studio links a card to its layers with it.
The props schema
Export zod (v4) schemas for your components' props. Put .describe() on every field a person sets. Studio shows it under the field's label.
import { z } from 'zod';
export const genomeSchema = z.object({
seed: z.number().min(0).max(999).describe('Picks the pattern. The same seed always draws the same one.'),
palette: z.enum(['ember', 'ice', 'moss']).describe('The colours the pattern is drawn in.'),
});Pass it to the composition: <Composition id="Hero" schema={z.object({ look: genomeSchema })} ... />. Studio builds its form from the schema. Without a zod 4 schema, it infers the fields from defaultProps.
| Schema | Field |
|---|---|
z.enum([...]) | A select |
z.number().min(a).max(b) | A slider with a number box |
z.boolean() | A switch |
zColor() from @kinetographdev/core/zod-types | A colour input and a text box |
zTextarea() | A text area |
z.object({...}) | A labelled group |
The panel
For a field that needs more than a form, point it at your panel with zod meta. The value is the package name.
genomeSchema.meta({ kinetographPanel: 'my-plugin' })The panel is an ES module named by kinetograph.panel. It exports mount:
export interface PanelContext {
value: unknown; // the field's current value
schema: Record<string, unknown>; // the field's JSON Schema
path: string[]; // where the field is in the props
onChange(value: unknown): void; // sets the field and re-renders the preview
state?: unknown; // what the panel last passed to setState
setState(state: unknown): void; // keeps panel-only state in the browser
preview?: { overlay: HTMLElement; width: number; height: number; frame: number };
}
export function mount(el: HTMLElement, ctx: PanelContext): {
update(ctx: PanelContext): void; // the value changed from outside
unmount(): void;
};- Studio bundles the module for the browser. Bring your own React.
- The panel gets a plain element with no shadow root. Scope your CSS under a class of your own, and read Studio's variables (
--background,--foreground,--primary) to match the theme. preview.overlayis an element laid over the preview, in composition pixels, for guides or handles.- A render never runs the panel. If it throws, Studio shows an error box and the rest of the form keeps working.
Tools
A plugin can offer tools that a coding agent calls over MCP. Point tools at a module that exports a tools array.
import { z } from 'zod';
export const tools = [
{
name: 'roll',
description: 'Rerolls the unlocked slots of a genome, or all slots.',
input: z.object({ seed: z.number().optional().describe('Seed for the dice') }),
run: ({ seed }) => ({ /* data that serialises to JSON */ }),
},
];name is snake_case and unique in the plugin. input is a zod 4 object schema. run takes the parsed input and returns JSON data. It never writes files, renders or fetches. kinetograph mcp registers each tool as <short>_<name>, where <short> is the package name without its scope and with - turned into _. A roll tool in @kinetographdev/hexed becomes hexed_roll.
SKILL.md
Ship a SKILL.md that says what the plugin does in a sentence or two, lists its components, props and presets, gives two or three short examples, and says what to check with npx kinetograph still.