kinetograph.
Plugins

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, panel and tools point at the skill file, the panel module and the MCP tools module.
  • title and description name the plugin on its Studio card. Without a title, Studio shows the package name without its scope.
  • icon is an SVG that Studio draws in the text colour, so use currentColor.
  • components lists 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.

SchemaField
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-typesA 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.overlay is 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.

On this page