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/listprobe — 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/mcpprobeagainst a running Studio Pro, not copied from documentation. Raw fixtures live inmdl/backend/mcp/testdata/(tools.json,schema-entity.json). Re-capture per the onboarding procedure; do not hand-edit claims you have not verified.
| 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.versionis frozen at1.0.0across 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-readablecapabilities.yamlkeysavailable_sinceon 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.gateAttributeDefaults→ProjectVersion().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.
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).
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.
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 tool ⟶ CLOSED 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 $ID → drops 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. |
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 dialhost.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.
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 (primary → Primary); 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:
SetDataSource→CustomWidgets$CustomWidgetXPathSource(DataGrid 2 reaches it via auto-datasource, which reads the DataSource propertyPropertyTypeIDsreports from the def; ComboBox/Gallery map it explicitly in their shared def.json). Asort byclause becomes thePages$GridSortBar(sortItemswithattributeRef+sortDirection), and awhere [...]clause becomes the source'sxPathConstraint. (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 2columns): operation kind → pg shape, text-template keys take pg'sct:prefix. -
SetChildWidgets→ Widgets-typed slots (Gallerycontenttemplate), 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'sfilterslot) and custom-content cells (the column'scontentslot). The filter widgets are added towidgets.def.json. Their def.json always setsattrChoice: "auto", under which Studio Pro auto-binds the filter to the column attribute and rejects a non-emptyattributeslist — soSetAttributeObjectsis a deliberate no-op (emitting the derived attribute would drop the widget). -
SetExpression→ plain string key in the custom-widgetobject(Phase 2, verified live: ComboBoxcustomEditabilityExpression). -
SetTextTemplate→ct:-prefixed plain string; Studio Pro expands it to a fullPages$ClientTemplate.SetTextTemplateWithParamsrewrites{AttrName}placeholders to{1}refs backed byPages$ClientTemplateParameterattributeRefs 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). -
SetAction→Pages$NoClientAction/Pages$MicroflowClientAction/Pages$PageClientActionviacustomWidgetClientAction. Shape deviation from the native LightPage constructors: inside a custom widget'sobject, the microflow reference must nest inmicroflowSettings— pg silently drops a flatmicroflowkey 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.
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— realDROPof standalone documents (enumeration, microflow, page), which PED cannot delete at all.DROP ENUMERATION/MICROFLOW/ PAGEresolves the document's module + name and callsdelete_document {module_name, document_name}. Unlikesave_allthis 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: truemeans 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-saveflushes via Concord'ssave_allon Disconnect. Unreliable — keystroke automation. Concord'ssave_allsynthesizes a macOS Cmd+S (osascript → System Events), with two failure modes observed against 11.11 Beta:- 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 failsosascript is not allowed to send keystrokes (1002). - 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 Prosave_allbug (the synthetic Cmd-S hangs the IDE). Do not rely on--mcp-save; save manually (Cmd-S) in Studio Pro, and reportsave_allupstream to the Concord/Studio Pro team. The backend wiring is correct and will work unchanged oncesave_allis fixed; a silently-no-opsave_command_sentit cannot detect. Model-based gap-closers (delete_document,check_model) are unaffected — they do not use keystroke automation.
- 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
-
get_app_status— the API call works and returns well-formed{data:{running, runningUrl, projectName, …}}(exposed asGetAppStatus(), printed by--mcp-run). Butrunning/runningUrlare effectively a port/process probe, not the current session's console-managed runtime: it reportedrunning | :8080while 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;:8080was confirmed listening). So trust the API shape, but treatrunning: runningas "something is bound to the runtime port," which may be stale. -
run_app/stop_app(--mcp-runstarts the app) —⚠️ same UI-automation failure assave_all. They are "click the Run/Stop button" automations:stop_appreturned{"status":"command_sent"}but the app stayed running across repeatedget_app_statuspolls (2026-06-08, 11.11 Beta). So likesave_allthey report success without taking effect;run_appalmost 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.
Concord identity: concord-mcp (proto 2025-03-26), port 7783 (directly
container-reachable; no socat). Two behaviors to know:
tools/listis context-curated. Afterconcord__set_task_context, a subsequenttools/listreturns only a curated subset relevant to the declared task. The full 44 below come from a freshtools/listwith 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 · 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 | command_sent, no effect |
| App runtime | stop_app |
W | command_sent, app stays running |
| App runtime | save_all |
W | |
| 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).
- Open a project in the new Studio Pro; establish transport (direct or socat).
go run ./cmd/mcpprobe -url http://localhost/mcp -dial host.docker.internal:<port> -method tools/list→ save tomdl/backend/mcp/testdata/tools-<version>.json.- Diff against the previous
tools.json— added/removed/renamed tools. - Update the server identity and tool matrix tables above (new column).
- Re-run the capability gaps checks — especially delete / save / modules /
$IDexposure. Any gap that closed is a feature to build; note it here and indocs/11-proposals/PROPOSAL_mcp_backend.md. - Re-capture changed schemas (
ped_get_schema) for doctypes the backend uses; refreshtestdata/fixtures and any affected tests. - If a tool's input schema changed, update the backend call sites and the
version-awarenessskill if a workaround is needed for older versions.