Third MCP write tool, completing the "create + style + theme" write
surface for LLM clients. `insert_node` is the TS pen-mcp
equivalent's flagship capability — without it, LLMs can only
modify existing nodes; with it, they can build entire designs.
`crates/openpencil-shell-core/src/mcp.rs`:
- `McpCommand::InsertNode { kind, name, x, y, width, height,
fill_hex }`. fill_hex is `Option<String>` — color-bearing
shapes can carry a fill; structural nodes (frame/group)
pass None.
`crates/openpencil-shell-core/src/document/mcp_apply.rs` (NEW):
- `Document::apply_mcp_command(cmd)` — lifted to Document level
so InsertNode can reach Pages + the id allocator (variable +
theme commands still route to `var_table.apply_mcp_command`).
- `Document::next_node_id_seed()` — allocates a fresh id past
`max_node_id() + 1`, saturating_add-guarded so u64::MAX
returns 1 instead of colliding with NodeId::NONE.
- `parse_node_kind(s)` — accepts the same lowercase strings
the read-side tools (get_node, get_selection) emit, so an
LLM can round-trip a node's kind through read → modify →
re-insert without re-encoding.
- Pulled out of mutators.rs to keep that file under 800 lines.
`crates/openpencil-shell-core/src/mcp/tools.rs`:
- `InsertNode` (stateless tool struct) + `insert_node_snapshot()`
factory. No document snapshot needed — the tool doesn't need
to know the current state; the host's applier handles id
allocation + bounds installation.
- `McpTool::call` validates:
- All required args present (kind / name / x / y / width /
height); each `MissingArgument` carries the missing
name.
- kind in ALLOWED_KINDS (frame / group / rect / ellipse /
polygon / line / text / path).
- x / y / width / height parse as decimal i32 (negative x/y
allowed for nodes placed off the page-origin; width /
height must be non-negative).
- Optional fill_hex parses as #rgb / #rrggbb / #rrggbbaa.
- Returns `OkWithCommand(InsertNode)` on success.
Tests (6 added, 325 shell-core total):
- insert_node_validates_required_args — missing kind →
MissingArgument; invalid kind → InvalidArgument.
- insert_node_validates_numeric_args — non-numeric x →
InvalidArgument; negative width → InvalidArgument.
- insert_node_validates_optional_fill_hex — bad hex →
InvalidArgument.
- insert_node_returns_command_with_parsed_args — happy path;
every field round-trips into the McpCommand payload.
- apply_mcp_command_routes_insert_node — end-to-end: doc.empty()
→ apply InsertNode → new node lives on active page, bounds +
fill flow through, name matches.
- apply_mcp_command_rejects_invalid_node_kind — bad kind at
apply time → false (defensive re-check beyond the tool's
validation, so host-only call sites are safe too).
mutators.rs trimmed from 814 → 798 (apply_mcp_command extracted +
some redundant doc comments compacted). All OP-owned files now
under the 800 cap except 3 pre-existing tech-debt violators
(codegen.rs 867, widget_host/press.rs 840,
widgets/canvas_viewport.rs 839).
MCP write surface now: 3 first-party writes (set_variable_color,
set_active_axis_value, insert_node). The TS pen-mcp catalog still
needs: update_node, delete_node, move_node, copy_node, replace,
batch_design, design_skeleton — each extends McpCommand + the
same applier pattern.