fix(authoring): stop silently rewriting authored structure and affordances - #315
Merged
Conversation
Contributor
Author
|
Stacked on While retesting #313's own fixes during this build: |
…ances Building a full site through the MCP surface surfaced five places where an authoring call reported success and the result was quietly different from what was written. Each one failed silently, so the symptom always appeared somewhere else — a route that 404s, a script that never initialises, a filter that matches nothing. - A `postType` table created anywhere other than the Content UI arrived with no `title`/`slug` fields, because the UI seeds them client-side. `slugForTable` returns an empty slug for such a table, so every entry published without a public route. `createDataTable` now seeds the built-ins itself, so the invariant holds for the data API, MCP connectors and imports alike; caller-supplied fields still win on id collision. - HTML import preserved safe custom attributes on five module types only, so `<form data-x>` lost its hooks while `<div data-x>` kept them. A form is exactly what a progressive-enhancement script binds to, so the script exited before initialising with nothing logged. `base.form` now carries `htmlAttributes` and renders them alongside its generated wiring. - `<button type="reset">` became `type="button"`: `base.button` had no `buttonType` prop and hardcoded the attribute. A native reset control could not be authored at all. Adds the prop, maps it on import, renders it. - `<option value="">` became `value="<label>"` on import, and the renderer dropped an empty value even when stored. The conventional "no filter" option therefore became a real filter value named after its own label. Presence now decides on import, and `value` is always emitted. - Posting the exported .zip to `/import` (which takes a JSON bundle) answered "body does not conform to SiteBundleSchema", sending callers to hunt a bug in their bundle rather than at the wrong door. The handler now recognises the ZIP magic bytes and names `/import/archive`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DavidBabinec
force-pushed
the
fix/mcp-authoring-friction
branch
from
July 30, 2026 19:03
49342ca to
57efd15
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while building a complete site (13 documents, 21 routes, a post type, six reusable tables, a page-scoped runtime) entirely through the MCP authoring surface. Every issue here shares a failure mode: the call reported success and the stored result was quietly different from what was authored, so the symptom only appeared much later — a route that 404s, a script that never initialises, a filter that matches nothing.
What was wrong
A post type created outside the Content UI was unroutable.
buildPostTypeDefaultFields()is called client-side by the collection dialog, so a table created through the data API, an MCP connector or an import arrived with only its custom fields.slugForTable()returns''for a table with noslugfield, so every entry published with an empty slug and no public route. Diagnosing this from the outside is brutal: the table lists fine, entries save fine, statuses readpublished, and the routes 404.HTML import kept custom attributes on five module types only.
HTML_ATTRIBUTE_MODULESallowlists container/text/link/button/image, so<div data-x>survived and<form data-x>did not — and a form is precisely what a progressive-enhancement script binds to. The script exited at its firstquerySelectorwith nothing logged anywhere.<button type="reset">came back astype="button">.base.buttonhad nobuttonTypeprop and hardcoded the attribute in its renderer, so a native reset control could not be expressed at all.<option value="">came back asvalue="<label>". The import usedattr(el,'value') || textContent, so an explicitly empty value fell through to the label; the renderer then droppedvalue=""even once stored. The conventional neutral option became a real filter value named after itself, and filtering silently matched zero rows.Posting the exported
.zipto/importblamed the schema. That endpoint takes a JSON bundle; the archive belongs to/import/archive. The error —body does not conform to SiteBundleSchema— reads as a malformed bundle, not a wrong door.What changed
createDataTableseeds post-type built-ins server-side, so the routability invariant holds for every caller. Caller-supplied fields still win on id collision, and ordinary data tables are untouched.base.formgainshtmlAttributes(schema, control, render) and joins the import allowlist, with its generated wiring excluded so nothing duplicates.base.buttongains abuttonTypeprop (button|reset); both the<button>and<input type=reset>import rules map it, and the renderer emits it.value./importdetects the ZIP magic bytes and names/import/archive.Verification
bun test— 6450 pass, 0 fail.tsc -bandeslintclean.type="button"; a data table stays non-routable; a JSON bundle is not mistaken for an archive; generated form wiring is not duplicated).Known gaps, not addressed here
Two bridge-side problems were reproduced but are not in this PR, because both look like state races in the editor sync layer rather than contained defects, and guessing at them risked more than it fixed:
site_write_code_assetpersisted its runtime config whilesite.filesstayed empty, so the script never published. The tool's own readback confirmed the file was in the store, and a hand-writtensite-documentPUT persisted it correctly — pointing at a stale-shell overwrite between the store and the save, in the same family as the known stale-bridge finding.site_insert_htmlreturned node ids for three documents whose subtrees never persisted (a page, a template, and a second page), reported as success each time. Re-inserting into the same document worked.Both are worth their own investigation with the sync layer in view.
🤖 Generated with Claude Code