Skip to content

feat(mcp): mockup tools to write and patch a spec and read and resolve its annotations - #859

Open
Upd4ting wants to merge 33 commits into
feat/builder-engine-mcp-corefrom
feat/mockup-mcp-tools
Open

Upd4ting wants to merge 33 commits into
feat/builder-engine-mcp-corefrom
feat/mockup-mcp-tools

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

⚠ sensitive: it stacks #858 and #857, so its diff shows both until they land.

Why

Iterating on a visual must be much faster as a mockup than through the builder. #858 adds the mockup engine: mockups/<name>/spec.json, drawn live on /mockup, with annotations a person leaves on it. An agent in Safe mode, the default in the dashboard chat, cannot write files. These four MCP tools let it write and patch a spec and read and resolve those annotations through the core MCP server. Task: ANT-2041. The dms-mockup skills (ANT-2042) use these names.

Stack

Base: feat/builder-engine-mcp-core (#857, the MCP server). The branch also merges feat/mockup-engine (#858), because the tools call its compiler and annotation store. Until #858 lands, this diff shows #858's files too.

Review only these commits:

  • 4a0eb9be feat(mcp): apply RFC 6902 JSON Patch operations to a JSON document
  • bb571aa1 feat(mcp): add the mockup tools
  • 0b2a6f60, 14ebda55 refactor(mcp): import the engine through its entry, and declare the tools as constants tested through MCP_TOOLS
  • a67986f2, 9c28b558 test(mcp): the engine's depth and duplicate-id checks through mockup_write_spec, and tests on a unique mockup under the working directory
  • In the merge commits, .dependency-cruiser/backend.cjs adds "mockup" to the tier under ["mcp"], so mcp may import mockup.

Once #857 and #858 merge, rebase this branch onto main. It then shrinks to those commits.

Tools

Tool Effect Arguments Answer
mockup_annotations read { mockup, status?: "open" | "resolved", page? } { annotations }, filtered
mockup_resolve_annotation builder { mockup, id } The resolved annotation; writes only annotations.json
mockup_write_spec builder { mockup, spec } { ok, errors: [{ path, message }] }
mockup_patch_spec builder { mockup, patch: RFC 6902 operations } Same as mockup_write_spec
  • mockup_write_spec validates exactly as the watcher does (parseSpec, then compileSpec against ListBlockTypes()). It writes mockups/<mockup>/spec.json atomically, pretty-printed, only when errors is empty, so a refused spec leaves the file untouched. Each path is a JSON pointer into the spec.
  • mockup_patch_spec applies the patch to the current spec on a copy, then validates and writes like mockup_write_spec. A failed test operation or a bad pointer is an error naming the operation index.
  • A refused spec is a normal ok: false answer. A bad name, a failed patch operation or a missing mockup or annotation is an isError result.
  • Every tool parses mockup with parseMockupName, so no tool reaches a path outside mockups/<name>/. Safe mode serves all four, because none is project.
flowchart LR
  agent[Agent in Safe mode] -->|mockup_write_spec / mockup_patch_spec| tool[MCP tool]
  tool --> parse[parseSpec + compileSpec]
  parse -->|errors| answer[ok: false, errors]
  parse -->|no errors| write[atomic write of spec.json]
  write --> watcher[mockup store watcher]
  watcher -->|SSE state| viewer["/mockup viewer"]
  person[Person on /mockup] -->|annotate| ann[annotations.json]
  agent -->|mockup_annotations| ann
  agent -->|mockup_resolve_annotation| ann
Loading

Proof

All run on the committed head after a clean pnpm --dir packages/dms run build:

  • pnpm run checks: Checks: 11 passed, 0 failed.
  • pnpm --dir packages/dms typecheck: passes.
  • MCP mockup tool and mockup engine tests (dist/test/unit/mcp/mockup-*.test.js, dist/test/unit/mockup/*.test.js): 67 passing.
  • pnpm --dir packages/dms test:unit at b2c15792: 2634 passing (15m). The later merge of feat(mockup): live JSON mockups rendered by the real block preview #858's final fixes (0f4b2d60) touches only the sketch check, a frontend type and their tests; the mockup and MCP tests above ran on it.

The tests each name the wrong implementation they catch, among them:

  • a write-then-report, where an invalid spec changes the file: the test checks that it stays byte-identical
  • a path join without parseMockupName: eight traversal names are refused by all four tools and nothing outside mockups/<name>/ is created
  • a tool declared project: Safe mode's tools/list must list all four
  • a patch that half-applies: the file stays unchanged after a failing test operation

A spec under mockups/<name>/spec.json is parsed at its boundary, validated
against the live block registry with JSON-pointer issues, its fixture:
references rewritten, TableView built through TableView.fromSource and
sketches checked like dms-ai's. A watched mockup streams its state and
annotations over SSE. /mockup renders DmsMockupViewer, dev only.
buildMockupSchema describes spec.json with one node branch per block
type, plus the sketch and componentName nodes. Each preview adapter now
carries both directions, so the schema accepts the format the compiler
validates: a block option given as a child, a form's section fieldIds.
scripts/mockup-schema.mjs writes data/mockup-spec.schema.json, and its
--check mode runs in CI next to docs:catalog:check.
The example spec and every non-controller docs example compile with no
issue; a bad option, an unknown type, a missing fixture and a forbidden
sketch are reported at their pointers; a block registered at runtime
validates. The store keeps the last good pages on a JSON error, reaches a
subscriber within a second of a save, and keeps every annotation of
concurrent creates. The rows route sorts, searches and pages, and stopping
the engine ends its event streams. src/mockup ranks with routes and dev.
A reference page for skill authors: the file layout, the spec envelope
and page options, nodes and their preview-format exceptions, fixtures,
controller blocks, sketches and their refused markup, the compiled
state, the HTTP API and its events, annotations, and the viewer.
…dren

GET /api/mockup answered 404: a bare @get() does not match a controller's
root, @get("/") does. The compiler now reports a child its parent does not
accept (Grid takes only GridRow; a block that is no container takes only the
children its block options name), the example spec wraps its cards in a
GridRow, and the name 'schema' is reserved for the static schema route.
…eview

The render message carries an optional padding flag, kept from the previous
state when absent; the mockup viewer sends each page's, padded by default.
…docs example

dms-control map failed on the unmapped src/mockup files. The mockups feature
covers them with a journey a run's built backend can drive: every mockup
route answers 404 outside development. The docs example of MockupState
declares the PreviewNode it names, which the docs check compiles.
…tory, refuse slash-separated URLs

- The events route ends its stream when the request's socket closes: the API
  only pipes the stream into the response, so a closed tab left its stream,
  listener and watcher behind.
- One recursive watcher covers mockups/ while anyone subscribes, so a mockup
  directory deleted and made again keeps reaching its subscribers instead of
  freezing on its last state.
- The sketch check refuses src and href written after a slash or a quote
  (<a/href=...>), which HTML parses as an attribute.
- A spec nested deeper than 100 JSON levels is refused with an issue instead
  of overflowing the compiler's stack.
…ng nodes, 404 a missing mockup's events

- The rows route sorts strings with a locale comparison, so "apple" comes
  before "apple pie", and rows missing the field come last both ways.
- Two siblings with one id are an issue at the second id: the preview gives
  both the same layout path, so one could not be traced back to the spec.
- GET :name/events answers 404 for a missing spec instead of an empty 200
  stream, and POST :name/annotations answers 404 unless page and path name a
  node of the spec now.
- The sketch check refuses image() and image-set() of a remote URL and an
  at-rule written with an escape.
…d read sketch CSS with escapes decoded

- The events route listens for the request's close before it reads the
  spec, and serves nothing to a client already gone.
- The sketch check decodes CSS escapes before matching, refuses any quoted
  remote URL in CSS (every image-set candidate), checks the CSS of style
  elements and attributes, and skips quoted attribute values when it looks
  for a tag's attributes (<a title=">" href=...>).
…s in one total order, refuse entities in style attributes

- The store watches mockups/ for its own entries and each subscribed mockup
  directory on its own, re-arming one that is made again: a recursive watch
  read every sibling, and one unreadable directory failed every stream.
  A directory that cannot be watched is logged, not thrown.
- Numbers and numeric text sort by value before other text, so mixed rows
  sort in one order whatever their input order, and "1.25" < "1.5".
- A sketch's style attribute may not hold character references (&#40;).
…er references

&quot; &apos; &amp; &lt; and &gt; are ordinary HTML escaping: they are read as
the characters they stand for before the CSS checks, so image-set(&quot;https://...&quot;)
is still refused, and any other reference (&#40;, &lpar;) still is.
@Upd4ting
Upd4ting marked this pull request as ready for review November 11, 2026 23:47

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant