Repository navigation
GUI Builder: MCP tools for editing forms - #6206
Conversation
The builder's MCP server could inspect, select, drag and save, but a client could not build anything. Add editing tools, each on the builder's own code paths and validated as the inspector validates the same change: guibuilder_component_types, guibuilder_get_form_xml, guibuilder_add_component, guibuilder_insert_xml, guibuilder_set_properties, guibuilder_rename_component, guibuilder_delete_components, guibuilder_set_layout, guibuilder_set_constraint, guibuilder_set_event_handler, guibuilder_toolbar_commands, guibuilder_set_binding and guibuilder_create_form. Each mutating call is one GuiDocument transaction run unattended: a single undo step for the user, abandoned whole when any part is refused. ComponentCatalog records the properties the inspector offers per type; a test opens the inspector for every type and fails when the two disagree. Tool arguments keep explicit nulls, which reset a property or remove a handler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZMcbjRgY2gZgT5nLW9RVG
|
Posted this in the wrong PR by mistake. Ugh.
|
… forms, reads - insert_xml checked BorderLayout regions after the paste, when effectiveBorderConstraint() had already moved a duplicate to a free region, so a fragment asking for a taken region landed elsewhere (or, with index, pushed the existing child out). The fragment's own layoutConstraint is now checked before it lands, index is refused in a BorderLayout (order there only reassigns regions), and a new BorderLayout child's region is written down. - create_form wrote bindingStrategy="none" only for Form and Dialog roots, so a Container root defaulted to a binding model that set_binding cannot turn off. Every root type now starts with none. - Read-only and refused calls no longer disturb the person at the keyboard. toolbar_commands list bypasses the transaction; an edit refuses while the developer is typing in place or dragging, instead of committing their text or cancelling their drag; the canvas is rebuilt and "Modified" shown only when a committed edit changed the form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZMcbjRgY2gZgT5nLW9RVG
|
Thanks, all three were real; they're fixed in 6ec5a4d.
New tests in
All 17 tests in that class pass on JDK 8. The full GUI Builder suite is running locally, and I'll add its result here when it finishes. Generated by Claude Code |
|
@shannah see:
|
…derLayout siblings - A refused MCP edit aborted its transaction, which re-parses the XML and replaces every element object, but the canvas was no longer rebuilt when nothing changed, so it, the selection and the inspector kept the old elements: canvas clicks did nothing and inspector edits never reached the saved file. An edit refused before changing anything now closes the transaction without the re-parse (nothing is recorded for an unchanged document) and restores the selection; one rolled back after a partial change always rebuilds the canvas. Tests assert the canvas stays bound to the document after both. - Setting a BorderLayout region wrote only the named child's constraint, so siblings whose region came from their order (or a duplicate) moved silently. Every sibling's current region is written down first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZMcbjRgY2gZgT5nLW9RVG
|
Both fixed in 8b354a0. You were right about the first one: my previous push introduced it, and its test asserted the broken behaviour.
Generated by Claude Code |
|
The full GUI Builder suite passes at 8b354a0 on JDK 8 ( Generated by Claude Code |
The ComponentCatalog Javadoc also says a ComponentCatalogTest fails if the catalog and the inspector drift apart. No such test exists in the PR or the tree, so nothing actually checks this. That's a misleading comment rather than a bug. |
…huge numbers - An edit that changed nothing -- the value already there, or refused after rewriting an unchanged attribute -- still set the document's modified flag (all isModified() has before a form's first save), so an untouched form asked to be saved and create_form/open_form refused; and it left the document's selection on the agent's target while the canvas showed the person's. Both are put back whenever nothing changed (GuiDocument.restoreModified). - mcpInteger saturated values beyond the int range instead of refusing them, and the table span checks (r + rs > rows) then overflowed and passed. Such values are refused, and the checks compare against the room left. - ComponentCatalog's Javadoc named a test that does not exist; it now names McpEditingToolsTest.theCatalogOffersExactlyWhatTheInspectorOffers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZMcbjRgY2gZgT5nLW9RVG
|
All four are addressed in 234363c.
New tests:
One finding outside this PR:
Generated by Claude Code |
|
The full GUI Builder suite passes at 234363c on JDK 8: 225 tests, 0 failures. Generated by Claude Code |
|
@shannah can you use an adversarial red-team to review the changes before submitting?
|
Review (shai-almog): - create_form now refuses while the developer is typing inline or dragging, like every other editing tool. - Tables inside an insert_xml fragment are checked: overlapping cells, cells outside the table and spans past its edge are refused, and children without a cell get a free one. - set_binding reports only what syncBindingModel said, and a meaningful status line is no longer overwritten by "Modified through MCP". Adversarial review: - An edit waits while a modal dialog is open (the developer's Save or form-switch question runs a nested event loop; an edit landing there was marked saved but never written), while an inspector text field has focus, and while a Code, Model or CSS pane is open. - insets are validated by LayeredLayout itself, as the inspector does (baseline only on top, whole px, no infinity). - insert_xml validates and normalizes every attribute the canvas or the generator reads: inspector properties (written back in canonical form, so enabled="TRUE" or constraint="password" no longer diverge between canvas and app), axis, cells and spans on every element, layeredInsets, guided anchors, positions, references, size policies and preferred sizes (several of which the generator emits into source as written). - Pasted names are sanitized like renames. - If the canvas cannot show a committed edit, the edit is undone and reported as an error instead of success. - A rollback restores the modified state; toolbar commands are written under canonical keys so a re-parse does not read as a change. - Handler names reject the identifiers the generator renames (record, var...). - The catalog reports the sample text a reset shows. - guibuilder_state reports editorDirty and developerBusy for clients deciding whether they may close the builder. - Existing bug the new safety net exposed: an empty container switched to BorderLayout threw on every canvas rebuild (its "Drop here" hint was added without a region), from the inspector too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZMcbjRgY2gZgT5nLW9RVG
|
Your three findings are fixed in bb5ced8. As you suggested, I also ran an adversarial red team on the whole change before pushing. Two independent reviewers each took one PR, without access to my reasoning. The CodenameOne one found 11 more defects. I checked each against the code, and they are all fixed in the same commit. Your findings
Red-team findings
An existing bug the new safety net exposed (not from this PR). An empty container switched to BorderLayout made every canvas rebuild throw "Cannot add component to BorderLayout Container without constraint". New tests in
All 27 tests in that class pass on JDK 8. The full GUI Builder suite is running at this head, and I'll post the result. Generated by Claude Code |
|
The full GUI Builder suite passes at bb5ced8 on JDK 8: 232 tests, 0 failures. Generated by Claude Code |
|
@shannah these aren't crucial since the complexity of resolving them might be non-trivial:
|
…, redo of reverted edits - create_form refuses a class name that the generated companion imports (com.codename1.ui.*, ui.layouts.*, properties.* and the single-type imports): a form named Form extended itself, one named Label turned every generated Label field into the form. A test checks the list against the core jar so a new core class cannot slip past it. - Guided alignment and match policies refuse when no reference component is visible on the canvas, instead of reporting success with nothing written. - An edit the canvas cannot show is reverted without a redo entry (GuiDocument.revertLastChange), so Redo cannot bring it back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZMcbjRgY2gZgT5nLW9RVG
|
All three turned out to be small, so they're fixed in 1a2cd4b.
Generated by Claude Code |
Summary
The GUI Builder's MCP server could inspect the designer (state, actions), select, open a form, drag and run save/undo/redo, but an agent could not build anything: there was no way to add a component, set a property, change a layout or attach a handler. This adds editing tools, each implemented on the builder's own code paths:
guibuilder_component_typesguibuilder_get_form_xmlguibuilder_add_componentguibuilder_insert_xml.guifragment (a whole section) via the paste path: unique names, references inside the fragment follow, free table cells; a BorderLayout region already taken is refusedguibuilder_set_propertiesnullresetsguibuilder_rename_componentrenameSelected, so relationships are repointed and names stay uniqueguibuilder_delete_componentsguibuilder_set_layoutguibuilder_set_constraintapplyGuidedSizePolicy/applyLayeredActionguibuilder_set_event_handleractionEventon types that fire action events; reports the companion source and whether the stub already existsguibuilder_toolbar_commandsguibuilder_set_bindingsyncBindingModel(unattended: the existing model is kept)guibuilder_create_form.gui+ companion (validated likecn1:create-gui-form, bindingnonefor every root type), opened and saved so it compiles; never overwritesHow
CodenameOneGUIBuilder.mcpEdit(operation, args)dispatches. Every mutating operation runs inside oneGuiDocumenttransaction underunattended(...)and is a single undo step for the user.abortTransaction()and rebuilds the canvas, because the rollback re-parses the elements.toolbar_commands listbypass the transaction.rectangleFree/occupiedTableExtentfor tables (overflow-safe; out-of-range integers are refused),renameSelectedfor names,pasteXmlfor fragments (with an extra type/layout/region check of the fragment first).model/ComponentCatalogwrites down whatpropertiesTaboffers per type. The inspector still builds its fields by hand, soMcpEditingToolsTest.theCatalogOffersExactlyWhatTheInspectorOffersopens the inspector for every palette type and the Form root and compares theguibuilder.property.*fields with the catalog; adding a field to one without the other fails.GuiBuilderMcpControllerregisters the tools. Their arguments are parsed with nulls kept (JSONParser.parseJSONdropsnullmembers by default, which would turn "reset this property" into an empty request).Why
The Codename One browser IDE (BuildCloud
browser-ide) runs this GUI Builder precompiled and exposes WebMCP tools to the browser's agent; a companion BuildCloud PR reaches these tools in process (gui_*). They work the same over the desktop builder's socket MCP server.Test plan
McpEditingToolsTest(20 tests, JDK 8, under xvfb): every tool through JSON-RPC into a running builder, plus regression tests for each review finding:xvfb-run mvn -pl javase -am test -Dcodename1.platform=javase) at 234363c: 225 tests, 0 failures.check-control-characters.py,check-author-tags.py,check-copyright-headers.shon the changed files.Found along the way and not fixed here: core's
MCPJson.parseparses with longs on, andJSONParserthen drops an integer written with an exponent (1e10,1E3) for every MCP tool.🤖 Generated with Claude Code
https://claude.ai/code/session_01SZMcbjRgY2gZgT5nLW9RVG