Skip to content

Latest commit

 

History

History
604 lines (543 loc) · 50.4 KB

File metadata and controls

604 lines (543 loc) · 50.4 KB

PED / MCP Tool Capabilities per Studio Pro Version

Studio Pro ships an embedded MCP server (the "PED" — Progressive Element Disclosure — server, exposing Mendix's "Maia" agent tools) on a local HTTP port. The mxcli MCP backend (mdl/backend/mcp/) is a client of this server: it routes model writes through PED so Studio Pro stays the authoritative serializer while the project stays open. See PROPOSAL_mcp_backend.md for the why.

The tool surface changes between Studio Pro versions. Each release can add, remove, or change tools — which directly expands or limits what the MCP backend can do. This document is the canonical record of which PED tools exist in which Studio Pro version and what capability gaps each version has. Update it whenever a new Studio Pro version is onboarded (procedure at the bottom).

Direction: ADR-0006 decides to make this knowledge machine-readable — a version-keyed capability table merged with a live tools/list probe — so the backend gates on it and an agent-facing capability report is generated from it. This document then becomes the human-readable narrative over that table. Until that lands, this doc + the scattered per-type rejections are the source of truth.

This is a developer reference. It is the sibling of WIDGET_BSON_VERSION_COMPATIBILITY.md for the MCP transport instead of on-disk BSON. For the cross-layer view — how MCP coverage compares to the MPR backend, MDL, and the Mendix platform — see the Backend Coverage section of ../01-project/MDL_FEATURE_MATRIX.md; this document is the MCP column's deep-dive.

Provenance. Every row below was captured live with cmd/mcpprobe against a running Studio Pro, not copied from documentation. Raw fixtures live in mdl/backend/mcp/testdata/ (tools.json, schema-entity.json). Re-capture per the onboarding procedure; do not hand-edit claims you have not verified.

Server identity

Studio Pro MCP server present serverInfo MCP protocol Captured
≤ 11.10 No
11.11 Yes mendix-studio-pro 1.0.0 2025-06-18 2026-06-05
11.12 Yes mendix-studio-pro 1.0.0 2025-06-18 2026-06-23

serverInfo.version is frozen at 1.0.0 across 11.11 and 11.12 even though the tool surface and behaviour changed. So the server version is not a reliable discriminator between Studio Pro releases. The machine-readable capabilities.yaml keys available_since on the server version and therefore cannot express an 11.12-only capability — features that vary by Studio Pro version must be gated on the project's Mendix version instead (e.g. gateAttributeDefaultsProjectVersion().IsAtLeast(11,12)). Until the table grows a Studio-Pro-version dimension, this per-version doc is the source of truth for the 11.11→11.12 delta below.

serverInfo.version is the MCP server's own version, distinct from the Studio Pro version. The MCP server first appears in 11.11; earlier versions have no endpoint and the backend must fail with an actionable error on connect.

Tool matrix

A tool's cell is the first Studio Pro version it was observed in (✓ = present, blank = absent, = n/a). cmd/mcpprobe -method tools/list is the source.

Tool 11.11 Purpose / used by the backend
ped_get_schema Schemas for element types ($constructor for create, $element for read). Backend: fetched before create/add.
ped_find_document Find docs by module + type. Must NOT be used for DomainModels$DomainModel (always exists, nameless).
ped_read_document Progressive read; JSON-pointer paths to descend. Backend: dirty-set read reconstruction.
ped_list_folder Immediate contents of a module/folder. (Not yet used.)
ped_create_module Create a module (+ its domain model). Flushes to disk immediately.
ped_create_document Create standalone documents (enum, microflow, page, …). "Never create domain models." Backend: CreateEnumeration.
ped_update_document Operation-based set/add/remove at JSON paths. Backend: entities, attributes, associations (incl. removes).
ped_check_errors Validate documents (run after the final write). Backend: after every write.
pg_read_page Pages only — separate read path. (Not yet used.)
pg_write_page Pages only — separate write path; PED is forbidden for pages. (Not yet used.)
oql_generate NL → OQL for a module. (Agent helper; not used.)
search_mendix_knowledge_base Docs/KB search. (Agent helper; not used.)
read_skill Load a Maia skill. (Agent helper; not used.)
glob List files in a virtual file domain. (Agent helper; not used.)
read_file Read a file in a virtual file domain. (Agent helper; not used.)
write_file Write Java/JS/CSS in a virtual file domain. Intentionally not used — these exist for Maia (the in-IDE agent with no disk access). Claude Code / mxcli run with direct filesystem access to the project, so source files are edited on disk directly; routing through the virtual FS would be pure overhead. Not a capability gap.

initialize instructs clients to first read the resource mendix://studio-pro/system-prompt (the Maia system prompt + PED contract).

11.12 changes (delta vs 11.11)

Captured live 2026-06-23 (cmd/mcpprobe -method tools/list). The 11.11 matrix above is otherwise unchanged; this section records only what differs. Tool count 16 → 18.

Change Tool Effect on the backend
Removed pg_write_page Breaking (was: MCP page authoring broken on 11.12). Migrated (#697): page.go's pgWritePage now calls pg_patch_page instead. Verified live — page create round-trips.
Added pg_patch_page Pages via JSON Patch (RFC 6902): create/whole-page-write = one {op:"replace", path:"", value:<full LightPage>} (the value is the same LightPage pg_write_page took, so the content builders are unchanged); patch existing = targeted ops (path must reference an existing element). mxcli routes both CreatePage and the mutator's Save() through the root-replace form (#697). Targeted-op ALTER (vs. the current read-modify-replace-whole) is a possible future optimisation.
Added list_modules Returns [{moduleName, writable, fromMarketplace}]. Closes the "No list-modules tool" gap (see below) — pure-MCP module enumeration is now possible, reducing the hard dependency on a matching local .mpr.
Added install_marketplace_module {moduleName, versionId, conflictResolution} — installs a Marketplace module into the open project. (Not used by the backend yet.)

Tools already present in 11.11 and unchanged (do not re-add as "new"): ped_list_folder, oql_generate, search_mendix_knowledge_base, read_skill, glob, read_file, write_file.

New authoring capability — attribute default values (implemented, domainmodel.go). PED's DomainModels$StoredValue.defaultValue is settable via a ped_update_document path-op (/entities/N/attributes/M/value/defaultValue); the create constructor still can't carry it, so it's set as a follow-up after the attribute exists (applyAttributeDefaults). Verified live on 11.12: enums store bare (Draft, not MES.WorkOrderStatus.Draft); PED accepts bare or qualified input but normalises to bare. Gated on the project Mendix version (11.12+), not the server version (frozen at 1.0.0 — see the caveat under Server identity). The entity/attribute $constructor schema is otherwise unchanged from 11.11; its text now hints DomainModels$Index and DomainModels$ValidationRule are addable the same constructor-then-path-op way — candidates to wire next, like defaults.

New authoring capability — navigation (implemented, navigation.go; issue #699). The project-level Navigation$NavigationDocument has no dedicated PED tool, but it is reachable through the generic document tools (ped_read_document / ped_update_document with documentType set and documentName omitted — project-level). CREATE OR REPLACE NAVIGATION on a web profile is authored as path-ops: scalar leaves are set in place (/profiles/N/homePage/page, /profiles/N/loginPageSettings/page), a currently-null element (notFoundHomepage) is set whole, and the menu (menuItemCollection.items, an array) is cleared then rebuilt — removes and adds go in separate calls (PED forbids batching add+remove on one array path). Menu items are Menus$MenuItem constructors: caption is a plain string (PED wraps it into Texts$Text), the action is Pages$PageClientAction (page ref under pageSettings.page), Pages$MicroflowClientAction (microflowSettings.microflow), or Pages$NoClientAction (container); sub-items recurse. Verified live on 11.12 by reproducing a full menu (3 page items + an Admin container with 2 children). Gates/limits: ped_check_errors cannot address the project-level nav doc, so the ped_update_document result is the validation gate (a bad page ref fails the op); native profiles and role-based home pages are rejected (not wired); and Studio Pro often answers a nav write with -32000 Request timed out while the edit still applies (slow nav re-render) — the backend surfaces an actionable hint rather than auto-retrying the non-idempotent op.

New authoring capability — entity access rules (implemented, security.go). "Security" is two different things over PED. The security documents are sealed: ped_read_document on Security$ProjectSecurity and Security$ModuleSecurity both return "Unknown document type", so module roles, user roles, demo users, and project security settings cannot be authored over MCP. But an entity's access rules are not in the security document — they live on DomainModels$Entity.accessRules (the domain-model document PED already authors). Verified live on 11.12: a rule's moduleRoles, per-member attribute/association refs, and access rights are the same qualified names mxcli already builds (ExpenseApproval.Expense.Title, ExpenseApproval.Expense_Employee, ExpenseApproval.Manager), so EntityAccessRuleParams maps 1:1 onto a DomainModels$AccessRule constructor add. The referenced module role must already exist. Hard limit — PED is add/modify-only for access control: DomainModels$AccessRule and DomainModels$MemberAccess can be added and their leaves set, but never removed ("Element of type … cannot be removed"). So mxcli can GRANT a new rule but rejects REVOKE and replacing an existing rule in place (it can't remove the old rule/members) — do those in Studio Pro. The executor builds the complete member-access list (every attribute + FROM-side associations + system owner/changedBy, per the CE0066 FROM-entity rule), so the add passes ped_check_errors.

Capability gaps (11.11)

These are the absences that bound what the backend can do. They are as important as the tools that exist — re-check each when onboarding a new version, since a gap closing (e.g. a delete or save tool appearing) unlocks features.

Gap Consequence for the backend Status to recheck each version
No delete-document tool DROP of any standalone document (enum, microflow, page) is impossible. Only entities/associations delete, via a ped_update_document remove op on the domain-model array. Test docs created via MCP cannot be cleaned up — they persist until the user closes Studio Pro without saving. Watch for a ped_delete_document / equivalent.
No list-modules toolCLOSED in 11.12 PED cannot enumerate modules. The backend must read modules/structure from the local mounted .mpr (hybrid model); -p must be the same project Studio Pro has open. 11.12 adds list_modules ([{moduleName, writable, fromMarketplace}]) → pure-MCP module enumeration is now possible; the local-.mpr dependency for module listing can be lifted on 11.12+. Other reads (structure, IDs) still use the hybrid model.
No save/flush tool ped_update_document edits stay in Studio Pro's in-memory model; the on-disk .mpr is stale until the user saves. Drives the dirty-set read router. Refined on 11.12 (observed live, test8): created documents (ped_create_document, pg_patch_page page-create) flush to mprcontents/ immediately — no Concord save_all needed for them — but update ops still stay in memory (an alter entity add attribute was visible to ped_check_errors yet absent on disk → CE1613 on an mx check of a mid-session disk copy). (ped_create_module already flushed immediately on 11.11.) Watch for a save tool / autosave; recheck the create-flush behavior each version.
Reads omit $ID Reads expose $QualifiedName only. Association refs need entity GUIDs, recovered from the local reader (= live $ID for saved entities). Reconstructed reads use synthetic IDs mapped to names. Recheck whether reads expose $ID.
Array reads omit primitive types A /entities/N/attributes read gives attribute names ($QualifiedName) but not their primitive type or documentation — those need a per-leaf read (/entities/N/attributes/M/type → a DomainModels$*AttributeType constructor; /…/documentation → string). Reconstruction recovers them with a single batched leaf read (enrichReconstructedEntities) so a dirty/session module reports real types (correct DESCRIBE + a reliable ALTER diff); it falls back to placeholder String only if the read fails. Recheck attribute-array read shape.
No in-place attribute-type change set /entities/N/attributes/M/type is rejected ("only allowed to set primitive or reference properties directly" — the type is a nested element), and a whole-attribute replace is rejected too ("only allowed to update elements …"). The only route to a new type is remove+add, which drops the attribute's $IDdrops the column data. So ALTER ENTITY … MODIFY ATTRIBUTE <type> is rejected over MCP; do it against a local .mpr (Studio Pro does an in-place migration PED can't). Watch for a type-change / migrate op.
Two write protocols Pages must use pg_*; everything else uses ped_*. The system prompt forbids PED for pages. Stable, but reconfirm.
ped_create_document doc-type whitelist The create tool accepts only certain document types; some model documents are rejected even though they have a $constructor schema. Confirmed off the whitelist: Microflows$Nanoflow ("… Did you mean: Microflows$Microflow?"), Projects$Folder (empty folders), and BusinessEvents$BusinessEventService (a $element, not a $constructor — its CREATE/ALTER are rejected in businessevent.go, but SHOW/DESCRIBE read the .mpr, DROP goes via Concord, and the published-event entities/constants it relies on are creatable). So nanoflows aren't creatable over MCP (CREATE/ALTER rejected — nanoflow.go; DROP works via Concord), and an empty folder can't be created. But ped_create_document accepts a folderPath per document, which auto-creates the whole folder path — so a document is placed in a (possibly nested) folder at create time (folder.go/resolveDocContainer). What you can't do: re-parent an existing document — /folderPath is not a settable property (so MOVE/DROP FOLDER/MOVE FOLDER are rejected), and pages can't be foldered (pg_write_page takes no folderPath). Re-probe the create whitelist each version.
No Java action document creation ped_create_document rejects JavaActions$JavaAction outright ("Document type 'JavaActions$JavaAction' cannot be created.") — a Java action is backed by a .java source file Studio Pro generates/manages, so the model document can't be created standalone. CREATE/ALTER JAVA ACTION are rejected with an actionable error (mdl/backend/mcp/javaaction.go); calling a Java action from a microflow is unaffected. Watch for a Java-action create tool.
No security document ops PED's ped_find/read/update/create_document reject every security type (Security$ModuleSecurity, Security$ProjectSecurity, …) as "Unknown document type" — only ped_get_schema knows them as nested elements. Concord exposes only security reads (audit_security, read_entity_access_rules, read_microflow_security, read_security_info). So security cannot be authored via MCP (module/user roles, entity access rules, GRANT/REVOKE, demo users, project security level) — neither server has a write path. Determine support with ped_read_document, NOT ped_find_document: find also reports the (supported) nameless DomainModels$DomainModel as "Unknown", so it is not a reliable probe. Watch for security write tools on either server.

Transport (per environment, not per version)

The server binds IPv6 loopback only ([::1]:7782 observed) and enforces a DNS-rebinding guard: the /mcp route requires HTTP Host: localhost (bare, no port). From a devcontainer:

  • Some sessions are reachable directly at host.docker.internal:7782.
  • Otherwise bridge on the host: socat TCP4-LISTEN:7783,reuseaddr,fork 'TCP6:[::1]:7782', then dial host.docker.internal:7783.

cmd/mcpprobe and the backend client pin the dial target while keeping the Host header localhost (-url http://localhost/mcp -dial host.docker.internal:<port>). The port can change between Studio Pro sessions — confirm with lsof on the host.

How the mxcli MCP backend uses this surface

Implemented (11.11): CREATE MODULE, CREATE/ALTER/DROP ENTITY, CREATE/DROP ASSOCIATION, CREATE ENUMERATION, CREATE/ALTER/DROP CONSTANT, CREATE VIEW ENTITY, CREATE MICROFLOW (broad activity + control-flow coverage), CREATE PAGE + ALTER PAGE (INSERT/DROP/REPLACE/SET property/DataSource/Layout), and CREATE WORKFLOW + DROP WORKFLOW + ALTER WORKFLOW, with a dirty-set read router that makes in-session edits visible.

CREATE/ALTER CONSTANT maps onto Constants$Constant ({name, type, defaultValue, exposedToClient}). Two PED-shape facts: the constructor type is a plain enum string limited to String / Integer / Decimal / Boolean / DateTime (Long / Enumeration / Binary are rejected, not coerced), and there is no documentation field (dropped). CREATE OR MODIFY sets the defaultValue and exposedToClient leaves in place; a type change is rejected because the model's type is a nested DataTypes$*Type element PED can't set directly (same constraint as an attribute's type — UpdateConstant reads the live type and refuses a mismatch). DROP goes through Concord's delete_document (no PED delete tool), like enumerations.

CREATE MICROFLOW maps a broad set of actions (variable/object/list changes, create/commit/delete/rollback, aggregate, list operations, retrieve, call micro/nano/java/javascript action, log/validation/download/close-page, show home page) and list operations (head/tail/filter/find/sort/union/ intersect/subtract, contains, equals). Some actions can't be expressed through PED's simplified action constructors and are rejected rather than mis-built: show page (the constructor omits the target page — pages go through pg_*), cast (the CastAction constructor exposes only outputVariableName, not the input variable or target type), and retrieve sorting / custom range and the list-range operation (PED's byDatabaseQuery input is entity + xPathConstraint + takeOnlyFirst only, and Microflows$Range exposes no settable fields). These would need a post-create element update PED's simplified constructors don't support.

ALTER ENTITY diffs the executor's rebuilt entity against the live model (name-keyed) and routes by the diff's shape: adds-only → ADD ATTRIBUTE, removes-only → DROP ATTRIBUTE, one-add-one-remove → RENAME (set the name leaf in place, preserving the attribute's $ID/column data), and no structural change → in-place entity & attribute documentation (primitive-property sets). An attribute type change is rejected (see the type-change gap above) rather than silently no-op'd; reliable detection depends on the enriched reconstruction carrying real types, so a documentation edit on a dirty module is never mistaken for a type change.

CREATE WORKFLOW maps the executor's workflow onto Workflows$Workflow (parameter + a linear Workflows$Flow of activities Start … End, connected via condition outcomes that may nest sub-flows). Activity-type coverage: Start/End, CallMicroflow (outcomes), single UserTask (task page, XPath/Microflow user targeting, task name/description, named outcomes each with a recursive sub-flow), Decision (ExclusiveSplit: expression + boolean/enum outcomes), ParallelSplit (concurrent paths), JumpTo, WaitForTimer, CallWorkflow (sub-workflow ref + parameter mappings — PED binds $WorkflowContext implicitly, so there is no parameterExpression field), WaitForNotification, MultiUserTask, and boundary events (interrupting/non-interrupting timers, with handler sub-flows) on user-task and call-microflow activities. MultiUserTask gotcha: unlike the single variant, its pageReference is a bare string (not a taskPage element), participiantInput is required (defaulted to AllTargetUsers), and its outcomes are plain value strings — so a multi-task outcome carries no per-outcome sub-flow (one with activities is rejected rather than silently dropped). Still rejected: AIAgentTask, annotations, and other niche activity types (clear error each). Type-name gotcha: PED's element type for the call-microflow activity is Workflows$CallMicroflowActivity, NOT the on-disk BSON $Type Workflows$CallMicroflowTask — they differ; use ped_get_schema Workflows$WorkflowActivity to enumerate the valid concrete subtypes. Workflow documents are addressed by qualified name (<module>.<workflow>), not a bare name. Entity references into a session-created module (a workflow's context entity, a microflow parameter) resolve because ListDomainModels/GetDomainModel reconstruct the session module's live entities from PED.

ALTER WORKFLOW uses ped_update_document path operations (NOT the page-style read-modify-whole-tree, because ped_read_document collapses nested workflow elements to their $Type). Implemented: workflow-level SET — display/workflowName/text + /title, description/workflowDescription/text, due_date/dueDate, parameter/parameter/entity. Note nested template/ parameter elements must be set by their leaf field (/workflowName/text), not replaced wholesale (PED rejects a whole-element set). Activity-level structural ops are wired via ref→index resolution (a shallow /flow/activities read matching caption/name, with @N disambiguation, top-level activities only): INSERT activity (ped add at index+1), DROP activity (ped remove at index), REPLACE activity (remove the slot, then add — a whole-element set by index is rejected). The outcome/path/branch/boundary-event ops follow the same array add/remove pattern one level deeper — on the activity's nested /flow/activities/<i>/outcomes (user-task outcomes, decision branches, parallel paths all share this array, differing only by element $Type) or /flow/activities/<i>/boundaryEvents; DROP reads the array to resolve the match→index first. SetActivityProperty sets primitive/reference leaves (page/taskPage/page, description/taskDescription/text, due_date); changing the kind of user targeting (XPath↔Microflow) is rejected because PED can't replace the nested element. Activity references resolve anywhere in the flow tree, not just the top level: resolve walks the flow depth-first (each activity, then its outcome sub-flows, then its boundary-event sub-flows — the same order as DESCRIBE, so @N numbering matches), reading each …/outcomes / …/boundaryEvents array (an outcome read exposes a collapsed flow field that signals a sub-flow to descend into) and returning the matched activity's containing array path. Every op then targets that path — e.g. insert after NestedCall adds into /flow/activities/1/outcomes/0/flow/activities, not the top level. So ALTER WORKFLOW is complete over MCP: workflow-level SET, all activity-level structural and property ops, on activities at any nesting depth.

CREATE OR REPLACE WORKFLOW rewrites an existing workflow in place via ped_update_document (PED has no document-replace tool), preserving the $ID. The workflow keeps its structural Start/End (PED refuses to remove either, and an index-less add lands after End in the array), so only the middle activities are swapped: the originals (indices 1..n-2) are removed, then the new middles are inserted just after Start. Each insert targets index 1 in reverse order — an explicit add index is validated against the array's original length (so an incrementing index can't grow the flow), but index 1 is always valid; reverse insertion leaves the middles in sequence. Workflow-level properties are set as in ALTER (title/workflowName/description/dueDate/documentation/parameter). Verified live (1-middle → 2-middle replace → [Start, NewB, NewC, End], validates clean).

CREATE MODULE routes through ped_create_module (which flushes to disk immediately) and registers the module in a session list merged into ListModules/GetModule(ByName), so a later op in the same run — e.g. create module X; create enumeration X.Y, or create module X; create entity X.Foo — resolves the freshly created module (the local reader does not yet know about it). For entities, GetDomainModel returns an empty synthetic domain model for a session module (ID mcp~dm~<module>, which moduleNameForDomainModel decodes back to the module name) since the reader has no on-disk domain model to read. Quirk: a module created via ped_create_module lags briefly before ped_update_document can mutate it (errors Module ... not found even though the create flushed), so pedUpdateDoc retries on that transient with a short backoff. The standalone-doc create paths (enumeration/page/microflow) resolve their module via GetModule (session-aware) rather than the reader directly. Note ped_create_module's success text is "Module 'X' created successfully.", NOT the SUCCESS-prefix the document ops use, so it has its own success check (contains "success").

ALTER PAGE is a read-modify-write on the pg tree: OpenPageForMutation loads the page via pg_read_page, the mutator edits the in-memory tree, and Save() writes it back via pg_patch_page (a root-replace patch). Supported in-place ops: INSERT (before/after a widget), DROP widget, REPLACE widget, SET DataSource, SET Layout, SET widget property (set (prop = value, …) on <widget>), and page-level SET Title (set (Title = '…') with no on clause) — plus the introspection the executor needs (FindWidget, WidgetScope, ParamScope, EnclosingEntity). The widget ref is the widget name (recursive tree search). The executor passes the AST position token ("AFTER"/"BEFORE"), so the mutator compares case-insensitively. SET maps the MDL property name (also case-insensitive) to its pg key: Class/Style → the widget's appearance; Caption/Content/Label → the ct:-prefixed client templates; ButtonStyle → pg's normalized enum; TabIndex/RenderMode/Editable/Name → direct keys; Visible → a conditional-visibility expression. Page-level SET Title maps onto the LightPage's top-level title string. Not yet mapped: page-level Url and pop-up settings (PopupWidth/Height/Resizable/CloseAction) — the LightPage schema is additionalProperties:false, so they aren't on the pg shape and are rejected before Save() (set them in Studio Pro); also unknown SET properties, column INSERT/REPLACE/property, design properties, pluggable-property SET, and page variables — each returns a clear error.

Pages use a separate protocol: pg_patch_page / pg_read_page (PED is forbidden for pages). The backend maps the executor's pages.Page (shell + LayoutCall slots + widget tree) onto the high-level page content. Note pg's container type is Pages$DivContainer (not Container); page reads (layouts/snippets/folders) delegate to the local reader because the executor resolves the layout through the container hierarchy. Validation success is signalled by a result text containing "success" (not the PED "SUCCESS"-prefix convention), and — unlike PED — there is no pg validation tool, so a bad attribute/page reference still writes "successfully" but shows a CE error in Studio Pro.

Widgets so far: DivContainer, LayoutGrid/Row/Column, TabContainer/TabPage, ActionButton, DynamicText, DataView, ListView, TextBox, TextArea, CheckBox, RadioButtonGroup, DatePicker (+ No/Microflow/Page/CreateObject client actions; page-variable / direct-entity / database / microflow data sources). Button styles are normalized to pg's canonical enum (primaryPrimary); an unknown style falls back to Default (pg rejects unknown values). A DataGrid 2 control bar is just the filtersPlaceholder slot holding action buttons. TextArea and the executor's RadioButtons (→ Pages$RadioButtonGroup) are attribute-bound inputs that share the same minimal attributeRef + ct:labelTemplate shape as TextBox; the server fills in the rest of their defaults (rows, render direction, placeholder, …), which are not yet mapped. Conditional visibility (visible: [xpath], i.e. VISIBLE IF) maps onto a Pages$ConditionalVisibilitySettings {expression} and is attached uniformly to every mapped widget; the MDL visible: property only ever produces an expression, so module-role / attribute / source-variable conditions are not mapped. (The static visible: false form sets a separate Visible value, not conditional visibility, and is not yet mapped.) Tab-page captions use the t:caption key (a plain string the server wraps in Texts$Text), not the ct: ClientTemplate prefix that button captions use. pg's widget union (from the tool schema) is the limit of native support: ActionButton, CheckBox, Content, DataView, DatePicker, DivContainer, DynamicText, LayoutGrid/ Row/Column, ListView, RadioButtonGroup, TabContainer/TabPage, TextArea, TextBox, plus CustomWidgets$CustomWidget (pluggable). No Pages$DataGrid — the legacy DataGrid is rejected; DataGrid 2 is a pluggable custom widget. Coverage grows one widget/data-source type at a time.

Pluggable widgets. The reference/dropdown selector — the Mendix 11 ComboBox (com.mendix.widget.web.combobox.Combobox) — is supported in both enumeration and association modes. Crucially, the MCP path does not build the BSON widget template the MPR writer must: it implements LoadWidgetTemplate with an mcpWidgetBuilder that records the engine's semantic property operations (SetAttribute/SetAssociation/SetPrimitive/SetDataSource) into a high-level pg object, and Studio Pro expands every default on pg_write_page (37 props filled from ~5 for ComboBox; 34 object + 19/column for DataGrid 2). This sidesteps the entire CE0463 "widget definition changed" template-mismatch class of bugs that the on-disk BSON writer hits, because the server owns serialization. One ComboBox quirk: the def.json enum mode maps only attributeEnumeration (the MPR template carries optionsSourceType's default), so the MCP backend infers optionsSourceType: "enumeration" — otherwise pg defaults it to association and prunes the enum binding.

Acceptance is registry-driven, not a curated list (Phases 1+2 of PROPOSAL_mcp_pluggable_widget_authoring.md, live-validated on 11.12): any widget the executor's shared registry resolves (project .mxcli/widgets/*.def.json → global → embedded — including defs produced by mxcli widget extract) is authorable over MCP. mdl/backend/mcp/widgets.def.json is demoted to an auto-datasource hint table (which property of a built-in widget is its DataSource, so the engine's auto-datasource pass fires for e.g. DataGrid 2); it is still MCP-owned and deliberately not merged into the shared widget registry, so it cannot change the MPR datagrid path. A widget absent from the hint table is still authorable — it just maps its datasource explicitly via its def.json. The builder translates the shared engine's storage-agnostic calls:

  • SetDataSourceCustomWidgets$CustomWidgetXPathSource (DataGrid 2 reaches it via auto-datasource, which reads the DataSource property PropertyTypeIDs reports from the def; ComboBox/Gallery map it explicitly in their shared def.json). A sort by clause becomes the Pages$GridSortBar (sortItems with attributeRef + sortDirection), and a where [...] clause becomes the source's xPathConstraint. (Page datasources have no grouping concept.)

sort/where are supported wherever the official metamodel has a source type that carries them (verified against the modelsdk branch's generated types): GridXPathSource (= pg CustomWidgetXPathSource, DataGrid 2 / Gallery / association ComboBox) and ListViewXPathSource (pg Pages$ListViewXPathSource, list views with a database source) both have xPathConstraint + sortBar. A DataView, by contrast, has no XPath source type — DataViewSource is context/parameter-only (pageParameter/snippetParameter/entityRef) — so a constraint/sort on a data-view database source is correctly rejected. This is a metamodel fact, not a pg limitation: emit the right source $Type and pg expands it. (List-view database sources must use Pages$ListViewXPathSource, NOT Pages$DataViewSource, or pg drops the constraint/sort.)

  • SetObjectList → generic object-list items (DataGrid 2 columns): operation kind → pg shape, text-template keys take pg's ct: prefix.

  • SetChildWidgets → Widgets-typed slots (Gallery content template), mapped recursively through the normal widget mapper so nested pluggable widgets and conditional visibility work inside a slot.

  • An object-list item's own Widgets-typed sub-slots are mapped recursively too, which gives DataGrid 2 column filters (textfilter/numberfilter/ datefilter/dropdownfilter → the column's filter slot) and custom-content cells (the column's content slot). The filter widgets are added to widgets.def.json. Their def.json always sets attrChoice: "auto", under which Studio Pro auto-binds the filter to the column attribute and rejects a non-empty attributes list — so SetAttributeObjects is a deliberate no-op (emitting the derived attribute would drop the widget).

  • SetExpression → plain string key in the custom-widget object (Phase 2, verified live: ComboBox customEditabilityExpression).

  • SetTextTemplatect:-prefixed plain string; Studio Pro expands it to a full Pages$ClientTemplate. SetTextTemplateWithParams rewrites {AttrName} placeholders to {1} refs backed by Pages$ClientTemplateParameter attributeRefs resolved against the entity context (the shared engine routes placeholder-bearing def-mapped texttemplates here — emitted literally, the braces fail Studio Pro's translatable-text parser).

  • SetActionPages$NoClientAction / Pages$MicroflowClientAction / Pages$PageClientAction via customWidgetClientAction. Shape deviation from the native LightPage constructors: inside a custom widget's object, the microflow reference must nest in microflowSettings — pg silently drops a flat microflow key there (verified live: flat → "Select a microflow"; nested → clean). Actions with parameter mappings, and other action kinds (save/cancel/close/delete/create/open-link/nanoflow), are rejected loudly — their pg value shapes are not yet pinned.

The authoritative pg shape sources are published by the server itself: read_skill (page-gen-common + references/common-objects.md / references/actions.md) and per-widget schemas at /pagegen/customWidgetsVFS/<widgetId>.schema.json (via glob/read_file). Consult these before probing new shapes. Widget-specific gotcha class: pg prunes properties made irrelevant by a selector primitive's default — the ComboBox optionsSourceType quirk above, and the Image widget dropping ct:imageUrl unless datasource is set to "imageUrl" (MDL: ImageType: 'imageUrl').

Client templates with {N} parameters (common in Gallery/DataGrid cells) emit a full Pages$ClientTemplate with attributeRef/expression/sourceVariable parameters — otherwise the literal {1} would show. DataGrid 2 columns with custom-content child widgets or parameterised header templates, and any property op the builder doesn't translate, are rejected, not silently emitted with missing content. The broader consolidation (removing the hardcoded Go maps in widget_defs.go and migrating the MPR path to def.json) is deferred until after the engine replacement merges.

Data sources for DataView/ListView: page-variable (Pages$PageVariable), direct-entity (DomainModels$DirectEntityRef), and microflow (Pages$MicroflowSource wrapping Pages$MicroflowSettings {microflow, parameterMappings:[], outputMappings:[], progressBar:"None", asynchronous:false, formValidations:"All"}). Microflow sources with parameter mappings, and database sources with XPath/sorting, are not yet mapped.

Microflow support is now broad: name, parameters, return type, and a recursive object/flow graph (positions reused from the executor's layout engine, so the MCP-authored canvas matches the file-written one). Supported activities: declare/set variable; create/change/commit/delete/retrieve/rollback object; create list, change list, aggregate, list operations (head/tail, filter/find by expression or attribute, sort, union/intersect/subtract); show message; log; call microflow / nanoflow / java action; download file; close page; validation feedback. Control flow: if/else ExclusiveSplit + ExclusiveMerge, for-each/while LoopedActivity + break/continue. Rejected (PED can't express them faithfully): show page (constructor omits the page ref — pages are pg_*), cast, inheritance splits, rule-split conditions, contains/equals/range list ops, queue settings.

View-entity choreography (verified): ped_create_document DomainModels$ViewEntitySourceDocument {name}ped_update_document set /oql → entity add with source: {OqlViewEntitySource, sourceDocument: "<qualified>"} and each attribute carrying value: {OqlViewValue, reference: <column>} (without the OqlViewValue the entity is "out of sync", CE-6770). The source document's name must equal the view entity's name. Because there is no delete-document tool, dropping a view entity removes the entity but orphans its source document, and CREATE OR REPLACE of an existing view entity fails at the duplicate source-document create. See mdl/backend/mcp/ and the proposal. Operations outside the slice return a clear "not supported by the MCP backend" error via the generated unsupportedBackend base.

Concord (optional second client — gap-filler)

Some deployments run a second MCP server, Concord (a Studio Pro extension; concord-mcp), alongside the built-in PED server. Concord is not an authoring server — it has none of the ped_*/pg_* create tools — but it provides operational/refactor capabilities PED lacks. The backend uses PED for authoring by default and reaches for Concord only for these gaps. Configure it with --mcp-concord / --mcp-concord-dial (a second Client, dialed independently); it stays nil when not configured, and every Concord-backed op errors clearly if it's missing.

Wired so far:

  • delete_document — real DROP of standalone documents (enumeration, microflow, page), which PED cannot delete at all. DROP ENUMERATION/MICROFLOW/ PAGE resolves the document's module + name and calls delete_document {module_name, document_name}. Unlike save_all this is model-based, not keystroke automation, so it is robust. (Entities and associations still delete via PED's array-element removal — no Concord needed.)

  • check_model (--mcp-check) — domain-model consistency check after writes (PED has no validation for the live model). Parses {success, healthy, summary {errorCount, warningCount, …}, errors[], warnings[]} and prints a report to stderr on Disconnect. Model-based (robust). Note: healthy: true means zero errors, not zero warnings — the report shows both.

  • save_all (--mcp-save) — PED has no save tool, so writes live only in Studio Pro's in-memory model until the user saves. --mcp-save flushes via Concord's save_all on Disconnect. Unreliable — keystroke automation. Concord's save_all synthesizes a macOS Cmd+S (osascript → System Events), with two failure modes observed against 11.11 Beta:

    1. Permission. Needs macOS Accessibility on the responsible process. If Studio Pro is launched from a shell that exec's the binary directly, the responsible process is the terminal, not Studio Pro — relaunch via open -n -a "<app>" --args … (launchd → app is its own responsible process) and grant Studio Pro Accessibility. Otherwise it fails osascript is not allowed to send keystrokes (1002).
    2. The keystroke does not save, and hangs Studio Pro — confirmed broken in 11.11 Beta (2026-06-08). First observed from a devcontainer ({"status":"save_command_sent"}, no disk change). Then re-tested the authoritative way — from Claude Code in the Concord terminal, single active Studio Pro instance, app frontmost — and it still did not persist and hung Studio Pro while Concord tried to drive the save. So it is not the two-instance ambiguity; it is a genuine Concord/Studio Pro save_all bug (the synthetic Cmd-S hangs the IDE). Do not rely on --mcp-save; save manually (Cmd-S) in Studio Pro, and report save_all upstream to the Concord/Studio Pro team. The backend wiring is correct and will work unchanged once save_all is fixed; a silently-no-op save_command_sent it cannot detect. Model-based gap-closers (delete_document, check_model) are unaffected — they do not use keystroke automation.
  • get_app_status — the API call works and returns well-formed {data:{running, runningUrl, projectName, …}} (exposed as GetAppStatus(), printed by --mcp-run). But running/runningUrl are effectively a port/process probe, not the current session's console-managed runtime: it reported running | :8080 while the Studio Pro runtime console was empty — because an orphaned runtime from a previous run was still bound to :8080 (restarting Studio Pro doesn't kill the separate runtime process; :8080 was confirmed listening). So trust the API shape, but treat running: running as "something is bound to the runtime port," which may be stale.

  • run_app / stop_app (--mcp-run starts the app) — ⚠️ same UI-automation failure as save_all. They are "click the Run/Stop button" automations: stop_app returned {"status":"command_sent"} but the app stayed running across repeated get_app_status polls (2026-06-08, 11.11 Beta). So like save_all they report success without taking effect; run_app almost certainly behaves the same. Wired correctly (will work once Concord's UI automation is fixed), but don't rely on --mcp-run/stop_app — start/stop the app manually in Studio Pro. Report upstream.

Pattern (important): Concord's model-editing tools work (delete_document, check_model); its UI-automation tools that synthesize button clicks / keystrokes do not in this environment (save_all, stop_app, run_app all return a *_command_sent status with no actual effect, and save_all can hang Studio Pro). get_app_status is read-only and returns valid data but reflects raw port state (can be a stale runtime). Net: only delete_document and check_model are dependable today.

Full tool inventory (44 tools, captured 2026-06-15)

Concord identity: concord-mcp (proto 2025-03-26), port 7783 (directly container-reachable; no socat). Two behaviors to know:

  • tools/list is context-curated. After concord__set_task_context, a subsequent tools/list returns only a curated subset relevant to the declared task. The full 44 below come from a fresh tools/list with no task context set. Always re-probe without a task context to see everything.
  • No Maia / agent-invoke tool. Like PED, Concord exposes tools an agent uses, not a "run the Maia agent" entrypoint. The concord__* tools are agent self- management helpers (calibration, friction journal, tool discovery), not Maia.

Wired/tested status: ✅ dependable · ⚠️ UI-automation, unreliable (see above) · ○ present, not yet wired/vetted. R = readOnlyHint.

Category Tool R/W Status / note
Domain model rename_entity W ○ in-place, identity-preserving rename (PED only does set /name)
Domain model rename_attribute W ○ identity-preserving
Domain model rename_association W ○ identity-preserving
Domain model rename_module W ○ identity-preserving
Domain model rename_document W ○ identity-preserving
Domain model set_documentation W ○ set element documentation
Domain model arrange_domain_model W ○ auto-layout entities/associations
Domain model delete_model_element W ○ delete entity/attribute/association (PED already removes these via array op)
Constants/enums rename_enumeration_value W ○ identity-preserving
Microflows modify_microflow_activity W ○ mutate an activity by 1-based position
Microflows insert_before_activity W ○ sequence an activity
Microflows set_microflow_url W
Pages delete_document W ✅ real DROP of standalone docs (wired)
Pages exclude_document W
Pages generate_overview_pages W ○ CRUD list + new-edit pages
Navigation manage_navigation W ○ menu structure, home page, role gating (Concord convenience; mxcli now authors web-profile nav directly via generic ped_update_document on Navigation$NavigationDocument — see below)
Project settings read_configurations R
Project settings set_configuration W
Project settings read_runtime_settings R
Project settings set_runtime_settings W
Project settings get_active_run_configuration R ○ which run config will run
Validation/diag check_model R ✅ domain-model consistency check (wired, --mcp-check)
Validation/diag check_project_errors R ○ full-project "Check All Errors" — reported stubbed in Concord
Validation/diag analyze_project_patterns R ○ architectural pattern/anti-pattern scan
Validation/diag get_last_error R
Validation/diag get_studio_pro_logs R
Security (read) audit_security R ○ full security audit (intended)
Security (read) read_entity_access_rules R
Security (read) read_microflow_security R
Security (read) read_security_info R ○ project security overview
App runtime get_app_status R ✅ valid data, but raw port probe (can be a stale runtime)
App runtime run_app W ⚠️ UI automation — command_sent, no effect
App runtime stop_app W ⚠️ UI automation — command_sent, app stays running
App runtime save_all W ⚠️ UI automation — does not persist and hangs Studio Pro; save manually
Project refresh_project W ○ force re-scan of project dir for external file changes
Concord meta concord__session_context R ○ session-start briefing (call once)
Concord meta concord__set_task_context W ○ declares task → curates subsequent tools/list
Concord meta concord__find_tool R ○ free-text "which tool for this task"
Concord meta concord__verify R ○ verify a high-level workflow (e.g. entity_created) actually applied
Concord meta concord__diagnose_project R ○ parallel project-state probes (SP running? .mpr locked?)
Concord meta concord__calibration_summary R ○ C4 calibration journal summary
Concord meta concord__record_calibration W ○ append (claimed, actual) calibration entry
Concord meta concord__silence_friction W ○ suppress friction-wire emission for a tool
Concord meta concord__tried_approaches R ○ prior friction occurrences for a pattern/tool

Highest-value unwired gap-closers (would fill PED limits the matrix currently marks unsupported, pending probe + identity-preservation vetting per ADR-0002 and the "don't fake identity ops" rule): the rename_* family (true renames vs PED's set /name). (Navigation is no longer in this list — mxcli authors web-profile navigation directly over PED; see the navigation note below.) check_project_errors and refresh_project are also candidates. delete_model_element is low priority (PED already removes entities/associations).

Onboarding a new Studio Pro version

  1. Open a project in the new Studio Pro; establish transport (direct or socat).
  2. go run ./cmd/mcpprobe -url http://localhost/mcp -dial host.docker.internal:<port> -method tools/list → save to mdl/backend/mcp/testdata/tools-<version>.json.
  3. Diff against the previous tools.json — added/removed/renamed tools.
  4. Update the server identity and tool matrix tables above (new column).
  5. Re-run the capability gaps checks — especially delete / save / modules / $ID exposure. Any gap that closed is a feature to build; note it here and in docs/11-proposals/PROPOSAL_mcp_backend.md.
  6. Re-capture changed schemas (ped_get_schema) for doctypes the backend uses; refresh testdata/ fixtures and any affected tests.
  7. If a tool's input schema changed, update the backend call sites and the version-awareness skill if a workaround is needed for older versions.