# Install and use Napkin

Napkin 0.5.0 is an ESM package for a React physical sketchbook and a local agent tools server. React and MCP use the same design document, evaluator, solver, and commands. Node 22 or newer is required for the CLIs. The React entry supports React 18 and 19 in modern browsers. No global CSS reset or remote service is required.

For the primary local Codex plugin workflow, see [Local plugin development](local-plugin.md). This guide covers embedding and the shared model/tool APIs. The MCP server now defaults to `~/.local/share/napkin/studies`; pass `--store` to continue using an existing repository `.napkin` directory.

## Install the local package

From this repository:

```sh
npm ci
npm test
npm run licenses
npm pack
```

From your React application's directory:

```sh
npm install /absolute/path/to/napkin/seankruzel-napkin-0.5.0.tgz
```

The package is not published to npm. Install the tarball, or publish to your own registry separately. `npm pack` builds the JavaScript and TypeScript declarations. The package contains core, React, server, MCP, CSS, and bundled dependency license notices.

## Embed a local workbench

```tsx
import { useState } from "react";
import { createStudy, restoreStudy, addTemplate } from "@seankruzel/napkin";
import { Workbench } from "@seankruzel/napkin/react";
import "@seankruzel/napkin/style.css";

export function EngineeringSketch() {
  const [study] = useState(() => {
    const saved = localStorage.getItem("my-design");
    if (saved) return restoreStudy(JSON.parse(saved));
    const study = createStudy("My design");
    addTemplate(study, "box");
    return study;
  });

  return (
    <Workbench
      study={study}
      onChange={(document) => {
        localStorage.setItem("my-design", JSON.stringify(document));
      }}
    />
  );
}
```

For SSR, create the `Study` on the server and pass its JSON snapshot to the browser; defer browser storage access until after hydration. `Workbench` renders on the server but browser file controls and pointer interactions activate after hydration. Memoize the study/controller; creating one during every render resets the model.

The workbench provides fixed XY, YZ, XZ, and ISO views of one parameter model. Draw rectangles, circles, rods, polygon plates, and freehand marks on any orthographic plane. Select parts by name, search, click, or drag-box. Save focused scenes for a single part, assembly, use case, or failure scenario, and choose Idea, Dimensions, or Connections detail. Scenario-linked scenes evaluate their overrides in the drawings, parameter sheet, and calculation traces; editing an override leaves baseline inputs intact.

Use `activeSceneId` and `onActiveSceneChange` to synchronize an external scenario selector with the drawing scope. Optional `parameterFocus: Ref[]` curates the default sheet for a domain demo; the user can still choose Show every parameter. Idea detail labels only selected parts, while Dimensions and Connections reveal the part names.

The local demo includes `/multicopter.html`, a quad/hex sizing example. Component inputs, scenario controls, result cards, and drawings use one study. Baseline edits update base quantities; payload/environment/failure scenes edit scenario overrides. The example is source code under `examples/react-demo/`, separate from the installable core API.

In Select mode, drag a part or the current selection to move it. Orthographic views move along their two visible axes; ISO uses the last chosen orthographic plane. Movement previews all linked drawings and commits one undoable, revision-checked edit on release. Bound positions keep their original parameter relationships through editable offsets. Scenario scenes store placement adjustments as overrides. Escape cancels a gesture; Fit drawing resets the framing after a move.

Freehand marks are persisted in metric world coordinates and appear in the linked views. Scene definitions, annotations, and source references live in optional `document.drawing`. Save them through an atomic `{ kind: "set-drawing", drawing }` edit. Existing schema-version-1 documents without drawing metadata still load. Deleting referenced parts or scenarios rejects dangling scene references unless cascade removal is requested.

Images, rough text, conversations, and audio can be captured beside a scene. Images can act as tracing references on their captured plane. Uploaded references are embedded in the JSON document (600 KB per upload). Automatic source interpretation is supplied by your application through `onInterpretSources`:

```tsx
<Workbench
  study={study}
  onInterpretSources={async ({ document, sources, sceneId }) => {
    // Your agent reads the references and returns core Edit[] proposals.
    return myAgent.proposeDrawingEdits({ document, sources, sceneId });
  }}
/>
```

The callback receives a snapshot and scene-specific sources. Returned edits commit atomically against the captured revision; a concurrent change rejects the proposal. Napkin does not bundle a transcription or multimodal AI provider. Shapes and freehand marks can be authored directly without an interpreter.

Dimension editing, inverse edits, attachments, calculation traces, independent or linked copying, undo/redo, JSON save/load, templates, and analytical checks remain available. Inverse editing and attachments are collapsed controls. Polygon coordinates are local metres; the parameter sheet exposes shape thickness assumptions. Canonical view switching and unfinished gestures are local UI state.

Core `study.previewScenario(scenarioRef)` returns an evaluated document with scenario overrides without mutating the baseline. Command `napkin_explain` and `napkin_solve` accept an optional `scenario` reference for the same evaluation context (explanation cannot combine scenario and run contexts).

## Core API

```ts
import { createStudy, scalar, pose, expression } from "@seankruzel/napkin";

const study = createStudy("Balsa block");
const width = study.quantity({
  name: "width",
  role: "input",
  unit: "mm",
  value: scalar(120, "mm"),
  assumption: "Measured stock width",
});
const height = study.quantity({
  name: "height",
  role: "input",
  unit: "mm",
  value: scalar(80, "mm"),
});
const depth = study.quantity({
  name: "depth",
  role: "input",
  unit: "mm",
  value: scalar(60, "mm"),
});
const block = study.part({
  name: "Block",
  geometry: { kind: "box", size: [width, height, depth] },
  pose: pose(),
});
const volume = study.quantity({
  name: "volume",
  role: "derived",
  unit: "m^3",
  expression: expression("w*h*d", { w: width, h: height, d: depth }, "m^3"),
});
const attachment = study.port(block, { name: "center", local: pose() });

study.edit([{ kind: "set", quantity: width, value: scalar(240, "mm") }], {
  expectedRevision: study.revision,
});
console.log(study.explain(volume));
```

Eight authoring methods create `quantity`, `part`, `port`, `joint` (`connect`), `relation` (`relate`), `law` (`act`), `observation` (`probe`), and `scenario` records. Every returned reference is immutable. Names are labels; IDs are identity. A `component` record (catalog options for a group of inputs) is created through `edit`.

Use `edit` with `create` nodes to author an entire assembly atomically, including forward references. `addTemplate` does this automatically. `replace` changes a record's spec with its existing ID and kind. `replace-geometry` changes a part shape. `set` edits an input or unknown; derived values are read-only. `rebind` installs a derived equation with an explicit binding table. `select` applies a component's catalog option to its inputs. `remove` either rejects dependent references or recursively cascades. Every mutation checks `expectedRevision`. Invalid units, reference cycles, geometry, mass properties, ranges, and unsatisfied hard constraints reject the batch and leave the prior revision intact.

Expressions allow numeric constants, explicit bound quantity symbols, `pi`, `e`, arithmetic `+ - * / ^`, parentheses, comparisons `< > <= >= == !=` with `and or not`, conditionals `condition ? a : b`, and `sqrt abs sin cos tan asin acos atan atan2 exp log log10 min max floor ceil round sign`. A number followed by unit symbols is a unit literal: `max(0 A, I - 2 A)`, `9.81 m/s^2`, `30 L/min`. A bound symbol always wins over a unit name, so a quantity bound as `g` is never read as grams. Assignment, property access, arrays, arbitrary functions and captured JavaScript are rejected. Length/mass/time calculations normalize to SI. Angular bindings normalize to radians, dimensionless in physical expressions, so torque × angular velocity yields watts. Offset units convert affinely: `25 degC + 13 K` is 38 degC; write temperature differences in K. `rpm` (= `cycle/min`), `rps` and `percent` are units. Because radians are dimensionless, converting an angular rate to `Hz`, `s^-1` or `min^-1` is rejected as `ambiguous_angular_rate`; declare rates in `rad/s` or `rpm`. Declared and display units survive persistence. Traces retain the equation, substitutions, dependency IDs, assumptions, revision, and provenance.

### Quantity roles and components

| Role | Holds | Notes |
|---|---|---|
| `input` | `value` | Editable. Optional `range` (valid design interval) and `tolerance` (`{ kind: "relative" or "absolute", value, distribution?: "uniform" or "normal" }`: half-width for uniform, σ for normal) |
| `derived` | `expression` | Read-only |
| `unknown` | `initial` | Resolved by design relations |
| `function` | `parameters: [{ name, unit }]`, `expression` | Callable in other expressions, e.g. `roadLoad(vTop)`. An operating point is one relation between two calls instead of a duplicated chain |
| `table` | `argumentUnit`, `points: [x, y][]`, optional `source` | Linear interpolation of a measured curve, e.g. `pumpCurve(Q)`. Outside the table is an `out_of_table` error |
| `rollup` | `operation: "sum", "min" or "max"`, `tag` | Combines every scalar quantity whose `tags` include the tag, so adding a tagged load or mass updates it |

Function and table quantities have no scalar value; `explain` describes them, and the workbench sheet lists only scalar roles.

A `component` record groups catalog options for a set of input slots: `{ name, slots: { kv: ref, … }, options: [{ name, source?, values: { kv: scalar, … } }], selected? }`. The edit `{ kind: "select", component, option }` writes an option's values into its inputs and records the selection. A scenario's `select: [{ component, option }]` applies an option as overrides; explicit overrides win.

A pose uses metric `position` inputs and a normalized `[x,y,z,w]` quaternion. Optional `rotation` is an input-bound `[X,Y,Z]` Euler rotation (radians or degrees), applied in X→Y→Z order in the local frame before the base quaternion. This permits geometry orientation to follow a quantity without storing another model in the renderer.

### Inverse editing and constraints

```ts
study.manipulate(
  volume,
  {
    kind: "resize",
    frame: "world",
    target: scalar(0.001, "m^3"),
    solveFor: [width],
  },
  { expectedRevision: study.revision, preview: true },
);
```

A preview returns candidate values without modifying the document. Remove `preview` to commit one undoable edit. The original derived equation remains intact. Choose free inputs explicitly; declared unknowns are always re-solved alongside them, and a derived-only solve list produces an error. Pass `scenario` to size inside a load case: free inputs that the scenario sets are written back to that scenario, other free inputs to the baseline. The local finite-difference damped least-squares solver handles scalar residual constraints and reports free degrees of freedom and rank deficiency. When the prior values do not converge it retries from starts spread across the declared ranges (`multi_start` diagnostic). `explore: true` searches every start and returns other converged solutions in `alternatives` (`multiple_solutions` diagnostic). It is bounded to 64 free quantities and 100 iterations per start and does not guarantee a global solution. The four-bar template's `branch` input chooses ±1 explicitly and is persisted.

`solve()` reports a solution without editing the document. Design edits resolve declared unknown quantities and validate the solved geometry before commit. A relation has a residual expression, target scalar, positive tolerance, geometric/coupling role, and applicable modes. The current solver enforces `design` relations. Runtime joint position/velocity/effort are not design inputs.

### Templates and analysis

`addTemplate(study, name)` returns `{ refs, scenario }`. Available templates:

`box`, `arm`, `winch`, `four-bar`, `slider-crank`, `suspension`, `quadrotor`, `hopper`, `steering`, `gripper`, `geared-motor`, `projector`.

Inputs are illustrative assumptions. Each template uses the eight public records. Analytical templates calculate sizing; they do not imply full simulated machinery. The four-bar's circle intersection formulas preserve the selected branch and reject unreachable geometry. The projector implements the ideal pinhole corner footprint, image width/center and keystone; use inverse editing with shift, tilt, or throw to target a scalar measurement. Its optical formulas do not implement calibration or full beam collision.

```ts
const scenario = study.scenario({
  name: "Width sweep",
  sweep: { quantity: width, values: [scalar(120, "mm"), scalar(240, "mm")] },
});
const result = study.run(scenario, { mode: "analytic" });
```

The pinned backend is `napkin-analytic@0.1.0`. It executes expression probes, world port distances, and signed AABB proxy clearances. A scenario evaluates one case, a `sweep` of one input, or a `profile` of duty-cycle segments (`[{ name, duration, overrides }]`). A check over several samples holds only if every sample passes; its value is the worst sample and `failedSamples` lists the failures. Aggregates are `instant` (last sample), `every`, `min`, `max`, `mean`, `rms` and `integral`. Over a profile, `mean`, `rms` and `integral` are weighted by segment duration, sample `time` is the segment start in seconds, and the integral of a W probe is in `(W)*s`. Sweep sample `time` values are indices, so an integral over a sweep is rejected. Kinematic mode supports scalar design sweeps, including parameter-driven mechanism formulas. Joint drive execution, assembly pose solving, arbitrary rigid-body dynamics, contact, spring ODE integration, and gear effort transfer return capability errors. Law records can be authored and persisted; the analytical backend does not execute forces or drives. Capsule drawing uses a cylindrical envelope; volume uses cylinder plus spherical ends. Meshes and AABBs are approximations, not CAD geometry or continuous clearance certification.

A valid model can fail a probe requirement without rejecting an edit. Runs include authoring revision, scenario, resolved values, assumptions, backend/version, seed, observations and status. Historical runs are explicitly stale after an edit. `explain(target, run)` rejects a stale context.

### Sketch text

`applySketch(study, text, { expectedRevision, prefix })` (MCP `napkin_sketch`) turns a compact model into records in one edit. Each record gets the ID `prefix:key`, so re-sending a line replaces that record, and new lines can refer to keys already in the study. Bindings are inferred from symbols that match keys. `parseSketch` returns the records without editing.

```text
const g = 9.80665 m/s^2                                   # shared constant, skipped by sensitivity
payload = 40 kg in 0..500 ± 10% "Rated cargo"             # input, range, tolerance (σ 10% for normal), assumption
mass := chassis + payload [kg] #mass                      # derived with unit and tag
total := sum of mass [kg]                                 # rollup of #mass quantities
roadLoad(v: m/s) := crr*mass*g + 0.5*rho*cda*v^2 [N]      # function
curve(Q: L/min) [m] := table 0: 25, 150: 21.9, 300: 12.5 "Datasheet"
vTop ?= 3 m/s in 0.01..30                                 # unknown
relate top: motorVoltage(vTop) == 0.95*voltage ± 0.0001 V
check range: range >= 10 km                               # unit from the literal or the limit quantity
probe energy: power [W] integral
scenario Steep: grade = 0.15, payload = 80 kg, motor = "2814 700Kv"
sweep Speeds: speed = 1..3 x 5 m/s                        # or a list: 1, 2, 3 m/s
profile Day:
  night 15 h: sun = 0
  noon 3 h: sun = 0.4
component motor "Motor": kv, R, mass
  2216 920Kv: 920 rpm/V, 0.12 ohm, 55 g "Datasheet A"
part wheel "Wheel": cylinder wheelR, 0.06 m at axleX, 0.25 m, wheelR rot 90 deg, 0 deg, 0 deg
ratio = 20 in 4..60 #motor                                # tagging a part key shows it on that part
```

`part` lines take `box x, y, z`, `cylinder r, length`, `capsule r, length` or `sphere r`, then optional `at x, y, z` and `rot X, Y, Z` (degrees by default). Each value is a number with a unit or a key. Cylinders run along their local z axis.

Comments start a line with `#` or `//`. Parse errors report the line (`invalid_sketch`); unit and expression errors come from the normal edit validation.

### Design-space analysis

`study.analyze(request)` (MCP `napkin_analyze`) is read-only and runs at the current revision, optionally inside a `scenario`. Every result carries `revision`, `method`, `limitations`, `evaluations` and `milliseconds`.

| `kind` | Request | Result |
|---|---|---|
| `sensitivity` | `outputs`, optional `inputs`, `step` (default 1%) | Central-difference derivative and elasticity (dy/dx · x/y) per input, strongest first. Defaults to non-shared inputs without a fixed `[a, a]` range |
| `grid` | 1–2 `axes` of input values, `outputs` | Outputs and checks per cell |
| `sample` | `outputs`, `count`, `seed` | Seeded Monte Carlo over input tolerances: mean, σ, p05/p50/p95, pass rate overall and per check |
| `optimise` | `objective`, `sense`, `vary` (ranged inputs) | Best values with every check as a constraint, `feasible`, `activeChecks`. Latin-hypercube seeds, then Nelder–Mead; a local search |
| `feasible` | `vary` (one ranged input), `samples` | Intervals where every check passes, the limiting checks at each edge, and `byCheck` intervals per requirement |
| `catalog` | `components`, `outputs`, optional `rank` | Every option combination with outputs and failed checks, feasible rows first |

Analyses run on a compiled plan (`Plan`). Units are checked once by the unit-aware evaluator, which must reproduce the plan's baseline exactly; later evaluations are SI floating point, about 50× faster on the multicopter. Unknowns are re-solved for every evaluation. The plan evaluates expression checks only: geometric checks are listed under `limitations` and remain available through `run`. A conditional branch that the baseline never takes is not unit-checked until an evaluation reaches it.

### Copy and persistence

`copy` expands selected parts to owned ports, internal connections, local quantity dependencies, relevant derived quantities and probes. `paste` remaps IDs and expression binding tables. Independent copies get local quantities; shared copies retain quantity IDs intentionally. Quantities marked `shared: true` can be retained as external references. External references require `reject` or `retain`; `detach` is an explicit capability error when unresolved external references exist. Paste placement supports literal translation, including offset quantities for parameter-bound positions.

`snapshot` and `restoreStudy` round-trip schema version 1 documents. Restore validates IDs, references, units, shapes and constraints. `study.restore(document, { expectedRevision })` replaces an existing model under a fresh revision; snapshots do not reuse old revision tokens when restored through a command. Undo/redo also allocate fresh revisions. Histories are limited to 100 edits and retained within the active process, not persisted across server restarts. Any external process edit resets this process's undo history.

## Connect an agent with MCP

Configure your MCP host with the installed executable and an absolute store directory:

```json
{
  "mcpServers": {
    "napkin": {
      "command": "/absolute/path/to/react-app/node_modules/.bin/napkin-mcp",
      "args": ["--store", "/absolute/path/to/designs"]
    }
  }
}
```

Or run from this repository with `node /absolute/path/to/napkin/dist/mcp/cli.js --store /absolute/path/to/designs`. Standard output contains only MCP messages; diagnostics go to stderr.

The server implements [MCP stdio framing](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio) and supports both legacy initialization through protocol `2025-11-25` and modern `2026-07-28` per-request metadata/discovery. Tool input schemas use JSON Schema 2020-12 and Zod validation. The stdio server has no SDK runtime dependency. The self-contained UI bundles the official MCP Apps SDK; both SDKs are used in interoperability checks.

| Tool                            | Purpose                                                 |
| ------------------------------- | ------------------------------------------------------- |
| `napkin_capabilities`           | Discover supported and unsupported analyses             |
| `napkin_list` / `napkin_create` | Find or create persisted studies                        |
| `napkin_open`                   | Open a study in an MCP Apps or legacy MCP-UI host       |
| `napkin_snapshot`               | Read the authoritative document and revision            |
| `napkin_template`               | Add a parameterized reference design atomically         |
| `napkin_edit`                   | Batch-create/edit/remove any authoring record           |
| `napkin_manipulate`             | Preview or commit inverse scalar editing                |
| `napkin_solve`                  | Inspect scalar constraints and diagnostics              |
| `napkin_sketch`                 | Create or replace records from compact sketch text      |
| `napkin_analyze`                | Sensitivity, grid, Monte Carlo, optimise, feasible, catalog |
| `napkin_show`                   | Draw or redraw a napkin card for this conversation turn |
| `napkin_annotate`               | Add or remove a note on a napkin card                   |
| `napkin_explain`                | Inspect equations, units, assumptions, dependencies     |
| `napkin_run` / `napkin_runs`    | Execute probes/sweeps and inspect saved/stale runs      |
| `napkin_copy` / `napkin_paste`  | Duplicate a design graph with explicit reference policy |
| `napkin_restore`                | Replace from a validated JSON document                  |
| `napkin_undo` / `napkin_redo`   | Navigate this process's edit history                    |

Typical agent workflow: capabilities → create/list → snapshot → template/sketch/edit → explain/run/analyze → snapshot. After a conflict, fetch a fresh snapshot and reconsider the edit. Do not retry with a guessed revision.

Example `napkin_edit` arguments:

```json
{
  "studyId": "returned-study-id",
  "expectedRevision": "revision-from-snapshot",
  "edits": [
    {
      "kind": "set",
      "quantity": { "id": "width-quantity-id", "kind": "quantity" },
      "value": { "value": 240, "unit": "mm" }
    }
  ]
}
```

## Napkin cards

A napkin is a small card for one turn of a conversation: the view the agent draws to show what it is talking about, rather than the whole design. `napkin_show({ studyId, page })` draws one; the page holds a title, one view (`XY`, `YZ`, `XZ` or `ISO`; default `XZ`), the parts in play (all parts by default), up to 6 callouts (quantity or expression-check references, with an optional label), an optional scenario, a one-line verdict and optional agent notes. `napkin_show({ studyId, pageId })` redraws an earlier page against the current model. The tool returns `{ studyId, napkin }` and links the `ui://napkin/card` MCP Apps resource.

`renderNapkin` precomputes the card: part outlines as SVG paths fitted to a 400 × 220 drawing, formatted callout values with pass/fail from the checks that use them, the failing checks, and each part's numbers. A part's numbers are the quantities tagged with its key (`ratio = 20 #motor` for `part motor`), then the inputs that size its geometry. The card needs no evaluator or unit library and loads at about a third of the workbench bundle.

On the card the user can tap a part to see its numbers, edit an input in place (a revision-checked `napkin_edit`), and pin a note at a point or on a part (`napkin_annotate`). After a change, each callout shows its value from when the page was drawn, and the header marks the page as updated. Every action updates the host's model context with what the user touched; **Send to chat** posts it as a user message. Pages live in `document.drawing.pages`, are pinned to a fingerprint of the physical model so notes never make them stale, keep the 50 most recent, and are cleaned up when a referenced part is removed with `cascade`.

The full workbench remains available through `napkin_open`.

## MCP Apps hosts

Napkin implements the [official MCP Apps standard](https://github.com/modelcontextprotocol/ext-apps) using `@modelcontextprotocol/ext-apps`. It exposes:

- Resource: `ui://napkin/workbench`, MIME type `text/html;profile=mcp-app`.
- Tool: `napkin_open({ studyId })`, with `_meta.ui.resourceUri` for hosts that negotiate the UI extension.
- Tool result: `structuredContent: { studyId, document }`, plus a JSON text fallback.

Hosts negotiate initialization capabilities like this:

```json
{
  "extensions": {
    "io.modelcontextprotocol/ui": {
      "mimeTypes": ["text/html;profile=mcp-app"]
    }
  }
}
```

After calling `napkin_open`, the host reads the linked resource and loads it in its sandbox. The app connects through the official SDK, receives the tool result, and calls Napkin tools through the host. Host theme/style updates, sizing, cancellation, and teardown use the MCP Apps lifecycle. Resource metadata declares empty external connection/resource/frame domains. All JavaScript and CSS are bundled inline; the UI needs no CDN, bridge URL, or bearer token.

The workbench retains canonical XY/YZ/XZ/ISO drawings, selection, drag movement, deletion, scenarios, and calculations. Its `McpAppController` serializes commands, submits current revision tokens, and refreshes agent edits every second. A stale edit is rejected and surfaced to the user. The initial snapshot is refreshed before the editor becomes available.

For an older MCP-UI host, call `napkin_open({ studyId, legacy: true })`. Its tool result additionally contains an embedded `text/html` resource. That UI sends `{ type: "tool", messageId, payload: { toolName, params } }` actions; the host must execute them and reply with `ui-message-response`, preserving `messageId` and returning `payload.response` or `payload.error`. Legacy mode is explicit because the two host protocols have different handshakes. Plain MCP clients receive the JSON result without an embedded resource by default.

The app adds **Refresh drawing**, **Expand drawing / Return to chat**, and **Discuss drawing** controls. Expansion uses the host's advertised display modes; discussion is available when the host accepts text messages. Discussion sends a user message only when clicked. Hosts supporting context updates receive a compact study/revision/scene/part summary after committed edits and scene changes, without automatically triggering a conversation. Source uploads and full document payloads are excluded from that summary.

Custom hosts may also use the exported `McpAppController` from `@seankruzel/napkin/react`, supplying `{ studyId, document, callTool }`; `callTool` receives `{ name, arguments }` and returns the MCP tool result. Call `close()` on unmount. The old `McpUiController` export remains an alias for existing integrations.

Run `npm run build && node scripts/browser-mcp-apps-check.mjs` to verify a real stdio server with the official MCP Apps host SDK, a scripts-only sandbox, strict CSP without network access or `unsafe-eval`, host-routed parameter edits, drag/delete/undo widgets, external agent edits, chat context and messages, fullscreen requests, theme changes, teardown, and legacy actions. This verifies protocol interoperability with that SDK; individual host products may impose their own tool permissions and presentation rules.

## Share the live React model with the agent

Start the local bridge with the **same absolute store directory** as MCP:

```sh
export NAPKIN_TOKEN="$(node -e 'console.log(require("node:crypto").randomBytes(32).toString("hex"))')"
napkin-bridge --store /absolute/path/to/designs --port 4317 \
  --origins http://127.0.0.1:8771,http://localhost:3000
```

For a live shared study, use `RemoteController` instead of a local `Study`:

```tsx
import { useEffect, useState } from "react";
import { Workbench, RemoteController } from "@seankruzel/napkin/react";
import "@seankruzel/napkin/style.css";

export function SharedWorkbench({
  studyId,
  token,
}: {
  studyId: string;
  token: string;
}) {
  const [controller, setController] = useState<RemoteController>();
  const [error, setError] = useState("");
  useEffect(() => {
    let closed = false;
    let connected: RemoteController | undefined;
    RemoteController.connect({
      url: "http://127.0.0.1:4317",
      studyId,
      token,
      onError: (error) => setError(error.message),
    })
      .then((remote) => {
        if (closed) remote.close();
        else {
          connected = remote;
          setController(remote);
        }
      })
      .catch((error) => setError(error.message));
    return () => {
      closed = true;
      connected?.close();
    };
  }, [studyId, token]);
  if (!controller) return <p>{error || "Loading design…"}</p>;
  return (
    <>
      <Workbench controller={controller} />
      {error && <p>{error}</p>}
    </>
  );
}
```

The controller polls changes every second, serializes its own fetches, and submits revision-checked commands. UI changes and MCP edits reach the same JSON store. The bridge is a local application RPC transport; it is not an MCP HTTP endpoint. It binds to loopback, requires a bearer token, and checks allowed origins. For a hosted multi-user application, mount the shared command layer behind your application's authentication and ownership checks rather than exposing the local CLI as a public service.

The filesystem store uses per-study directory locks plus atomic file rename. Locks wait up to five seconds. An interrupted writer may leave a `.json.lock` directory; verify that process has exited before removing that lock. No lock is silently stolen. Up to 100 runs per study are retained in storage. Store filenames use validated opaque IDs; tools do not accept arbitrary file paths.

## Verification and demo

```sh
npm test
npm run licenses
npm run demo:build
python3 -m http.server 8771 --bind 127.0.0.1 --directory demo-dist
# in another terminal
node scripts/browser-check.mjs
node scripts/browser-drawing-check.mjs
node scripts/browser-multicopter-check.mjs
node scripts/browser-drag-check.mjs
node scripts/browser-sync.mjs
node scripts/browser-mcp-apps-check.mjs
```

The Node suite covers all template round trips, independent sizing equations, unit normalization, atomic rollback, reference/geometry/mass validation, inverse edits, constraints, copying, branches, projector calculations, stdio MCP interoperability, modern protocol metadata, persistence and cross-process conflicts. Browser checks cover linked canonical projections, selection search and marquee, scene scopes, scenario edits and baseline isolation, drawing on YZ, freehand/source persistence, dimensions/traces, undo, equal edges, copy/paste, probes, and mobile layout. The synchronization check runs a real browser, bridge, and MCP subprocess together and verifies edits in both directions and stale-revision rejection.

`npm run licenses` checks installed production dependencies **and** packages included in the mathematical runtime and MCP Apps UI bundles. Only MIT/Apache-2.0 are accepted. The shipped third-party license notices are in `dist/vendor/THIRD_PARTY_NOTICES.txt`. Math.js's type-only BSD dependency is excluded from the bundle and installed package. Development tools and original Python study dependencies remain separately licensed.
