MCP clients often serialize numeric arguments as JSON strings.
Previously this caused validation errors on tools like node_move.
Now both the MCP server (zod) and AI adapter (valibot) coerce
string-encoded numbers to numbers, rejecting only genuinely
non-numeric values like 'abc'.
Fixes#207
Remove headless SceneGraph mode — the MCP server now always proxies
tool calls to the browser via WebSocket. One server, one architecture:
HTTP :7600 — /health, /rpc (CLI), /mcp (MCP Streamable HTTP)
WS :7601 — browser connects, executes tool calls against live editor
- Delete http.ts (merged into server.ts)
- Delete headless SceneGraph, open_file, save_file, new_document
- Delete canvaskit-wasm dependency (no headless rendering)
- Add ws dependency for WebSocket server
- Rewrite tests with mock browser over real HTTP+WebSocket
- Simplify bridge.ts to 6 lines (just imports and starts the server)
* feat(mcp): add export_image_file tool — saves PNG/JPG/WEBP to disk
The existing export_image tool returns base64 inline, which MCP clients
display in chat but don't save to disk. This new tool writes the image
directly to a file path, making exports usable for documentation,
mockup reviews, and CI pipelines.
Parameters:
- path: absolute file path to save (required)
- ids: node IDs to export (optional, defaults to all top-level nodes)
- format: PNG | JPG | WEBP (default: PNG)
- scale: 0.1–4 (default: 2)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mcp): add export_image_file tool and fix headless PNG rendering
**New tool: `export_image_file`**
Saves rendered PNG/JPG/WEBP images directly to a file path on disk.
The existing `export_image` returns base64 inline — useful for chat
but doesn't produce files. AI agents and CI pipelines need actual files.
Path validation runs before the expensive render call. The tool
respects `fileRoot` restrictions.
**Fix: headless PNG rendering now matches the editor**
1. `computeAllLayouts()` called after `render` creates nodes via JSX.
2. Bundled Inter font loaded from disk — tries `../fonts/` (installed
package) then `../../public/` (project root). The web-based
`fetch('/Inter-Regular.ttf')` fails in Node.js.
3. Text measurement uses `renderer.measureTextNode()` — the same
ParagraphBuilder the renderer uses to draw text. Measurement and
rendering use the same SkiaRenderer instance, same fontProvider,
same font shaping. Zero hacks.
Export pipeline:
ensureInterFont() → SkiaRenderer + loadFonts() →
setTextMeasurer(renderer.measureTextNode) →
computeAllLayouts() → renderNodesToImage()
**Tests added** (6 new tests in mcp-server.test.ts):
- export_image_file produces valid PNG after render
- export_image_file without ids exports all page children
- export_image_file without document returns error
- export_image_file with out-of-root path returns error
- JPG and WEBP formats produce non-empty output
- export_image_file is listed in tools
* Remove .cue directory and revert .gitignore changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Move computeAllLayouts into renderTree, reuse exportImage in MCP
computeAllLayouts was in the render tool but renderJSX has multiple
callers (render, replace_node, automation server). Moving it into
renderTree ensures layout is always computed after JSX rendering.
export_image_file in MCP server was duplicating export_image logic.
Now delegates to exportImage.execute and just handles the file write.
---------
Co-authored-by: Michael Magsuci <mcdmag@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
- Scope hover hit-test to current page (was searching all pages including
internal component page, causing ghost hover outlines)
- Frames/sections without visible fills or strokes are click-through
- Groups are always click-through (only children are hittable)
- Clipping parents reject hits outside their bounds
- Instances/components check children before falling back to fill check
- Switch test fixtures from material3.fig (55MB) / nuxtui.fig (82MB) to
gold-preview.fig (537KB) for fast dev runs, gate heavy fixtures behind
BUN_HEAVY_TESTS env var (enabled in CI)
Covers a few gaps in the existing MCP test suite:
- find_nodes filtering by type (wasn't exercised at all)
- create_shape with an invalid type enum (Zod enforcement)
- create_shape with a missing required param (Zod enforcement)
13 tests using InMemoryTransport from @modelcontextprotocol/sdk:
- Tool registration (all 76 tools listed with descriptions)
- new_document, open_file, save_file lifecycle
- Tool execution through MCP protocol (create, fill, delete, query)
- Error paths: no document loaded, nonexistent node, invalid file path
- Full workflow: new → create → nest → query → delete
Fix @open-pencil/core dep in MCP package: workspace:* for local dev
(pnpm publish resolves this to the actual version at publish time).