diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index f75d4e0bc..4bc455552 100644 --- a/.claude/skills/fix-issue.md +++ b/.claude/skills/fix-issue.md @@ -472,3 +472,4 @@ extracting `OffsetExpression`/`LimitExpression`. | A datagrid column bound to an association — `column c (attribute: Order_Customer)` — reports success from `mxcli exec` and then fails the build with `[error] [CE1613] "The selected attribute 'Mod.Order.Order_Customer' no longer exists." at Columns (1/1) of data grid 2`. Separately, there is no MDL spelling for the drop-down filter's association mode: `mxcli check` says `[MDL-WIDGET01] has no property \`refEntity\`` | Two unrelated defects behind one report. (1) The reference is **not representable**: `CustomWidgets$WidgetValue.AttributeRef` is typed `AttributeRef`, not the polymorphic `MemberRef`, so the association was qualified like an attribute and written as a dangling `AttributeRef`. (2) `dropdownfilter.def.json` mapped only `attrChoice`/`attributes`/`defaultFilter`, so every `baseType: 'ref'` property was unmapped and dropped | `mdl/executor/cmd_pages_builder_input.go` (`rejectAssociationAsAttribute`, `entityInChain`) wired into `mdl/executor/widget_engine.go` (the objectlist `attribute` case and the `Attribute` source); `sdk/widgets/definitions/dropdownfilter.def.json` (association mode); `mdl/executor/cmd_pages_describe_parse.go` + `_pluggable.go` + `_output.go` (round-trip) | **Establish that a shape is unrepresentable before designing a fix for it** — hand-patch the BSON and run `mx check`. A `DomainModels$AssociationRef` in that slot makes the project **UNLOADABLE** (`ArgumentException: Object of type 'AssociationRef' cannot be converted to type 'AttributeRef'`), and the assembly defining the type (`Mendix.Modeler.WebUI.dll`) has no `AssociationRef` member at all — so the only correct outcome is a refusal carrying both working forms. **`` on an ATTRIBUTE-typed widget property is permission to TRAVERSE a reference, not to bind one** — `attribute: Assoc/Attr` already worked and is what the XML is advertising; the DataGrid column is the only shipped widget where the two are easy to confuse. **A def.json `mode` is the whole feature** for an unauthorable widget mode — the engine already had the `association` operation and the `hasDataSource` condition, so the second half was a data change plus its DESCRIBE reader (without which describe→edit→exec silently reverts the filter to attribute mode). **0 errors from `mx check` does not prove the properties landed** — an unmapped property is silently dropped and the build is just as green; read them back with `mx dump-mpr`. Tests `cmd_pages_builder_assoc_as_attribute_test.go`, `widget_dropdownfilter_assoc_test.go`, example `mdl-examples/bug-tests/830-datagrid-association-filter.mdl`. upstream #830 | | An association's line anchors — where the connector attaches to the entity boxes in the domain model editor — are absent from `DESCRIBE ASSOCIATION`, and manual adjustments made in Studio Pro do not survive an mxcli round trip | `DomainModels$Association.ParentConnection`/`ChildConnection` (the string `"x;y"`) were **hardcoded** to `"0;50"`/`"100;50"` in BOTH writers and never read by either parser. Because every association write rebuilds the whole element, this was not an omission but active destruction: a documentation-only `alter association … set comment` reset them | `sdk/domainmodel/connection.go` (new: `ParseConnectionPoint`/`FormatConnectionPoint`, `Default*Connection`), `sdk/domainmodel/domainmodel.go` (fields → `*model.Point`), `sdk/mpr/parser_domainmodel.go` + `sdk/mpr/writer_domainmodel.go`, `mdl/backend/modelsdk/domainmodel.go` + `domainmodel_write.go`, `mdl/executor/cmd_associations.go` (`describeConnectionPoints`) | **A feature request that says "X is not exposed" may be hiding "X is destroyed"** — check the write path before scoping the read path. The A/B that settled it: a blank 11.13 app's own `Administration.AccountPasswordData_Account` stores `0;54/100;54`, so a Studio-Pro-authored association is a free fixture for "did mxcli overwrite this?" — no Studio Pro needed. **Learn the value's constraints from the LOADER, not from the shape**: hand-patch and run `mx check` — `"0.5;50"` dies with `StorageLoadException` (integers required) while `"0;500"` and `"-20;50"` load with 0 errors (no range check), so out-of-range values must round-trip untouched. **A zero value is not an absent value** — `{0,0}` is a real anchor (top-left), which forces the field to be a POINTER; a plain `model.Point` cannot distinguish "unset" from "top-left" and would silently rewrite it. **Fix both engines**: they share the semantic model, and a fix in one is invisible to a user on the other. **Emit unauthorable data as a COMMENT** — DESCRIBE output must stay re-executable, and inventing syntax (`@anchor(parent: bottom-left, …)`) would bake in a vocabulary the storage does not have: the pair is CONTINUOUS, not 8 named anchors (observed x values 0 9 11 17 18 47 49 50 65 77 78 84 87 100). **The marketplace is the sample** when you need to know what Studio Pro actually writes: `mxcli marketplace download ` gives real Mendix-authored models, and a module .mpk holds either a raw BSON `project.mpr` or an MPR v1 SQLite one — 88 coordinate pairs from three modules turned "looks like percentages" into a measurement (all 0..100; 85 of 88 pin one coordinate to exactly 0 or 100). **Rule a unit out from the model, not the values**: pixels is impossible because `DomainModels$EntityImpl` stores only `Location` and NO size — the box is sized by the editor from the name and attribute list, so a pixel anchor would have nothing to measure against. Not applicable to `CrossAssociation`, which has no connection properties and crashes Studio Pro if given them (#50). Tests `sdk/domainmodel/connection_test.go`, `mdl/backend/modelsdk/association_connection_test.go`, `sdk/mpr/writer_domainmodel_test.go`, example `mdl-examples/bug-tests/872-association-line-anchors.mdl`. upstream #872 | | An association's line anchors can be preserved but not AUTHORED — a scripted domain model cannot lay out its own connector lines, so `@Position(x, y)` gets you boxes and nothing gets you the lines between them | Feature gap, not a defect. `DomainModels$Association.ParentConnection`/`ChildConnection` had no MDL surface | `mdl/grammar/domains/MDLDomainModel.g4` (`SET ANCHOR`/`anchorPoint` — the ONLY grammar change), `mdl/visitor/visitor_association.go` (`anchorAnnotation`, `annotationParenPoint`, `anchorCoord`), `mdl/ast/ast_association.go` (`FromAnchor`/`ToAnchor` on both create and alter), `mdl/executor/cmd_associations.go` (`applyAnchors`, `describeConnectionPoints`) | **Look for an existing annotation before inventing one** — `@anchor(from:, to:)` already existed for microflow sequence flows, asking the same question (where does the connector attach), and `annotationParamName` already admitted FROM and TO, and `(x, y)` was already `annotationParenValue`: CREATE needed **zero** grammar. The two forms cannot be confused because the microflow one names its inner params (`(from: right, to: left)`) while a coordinate pair is positional. **Let the storage pick the value type**: the measured pair is continuous (x takes 14 distinct values across 88 samples), so named anchors were never an option — see the preservation row above for how that was established. **Silence must mean "preserve", not "default"** — naming one end sets it and omitting one keeps what is stored, which is what stops a `create or modify association` about the delete behaviour from flattening a hand-tuned line; the AST carries POINTERS so "not mentioned" and "mentioned as (0, 0)" stay distinguishable. **Reject what the LOADER rejects, at check time**: a fractional coordinate must error, not be truncated to 0 — Mendix refuses to open such a project, and a silently-wrong value in a file that still loads is the worse failure. **Prove DESCRIBE round-trips by parsing its own output** — asserting on a string literal passes against a formatter emitting something nothing can read. Tests `mdl/visitor/visitor_association_anchor_test.go`, `mdl/executor/cmd_associations_anchor_test.go`, example `mdl-examples/bug-tests/872-association-line-anchors.mdl`. upstream #872 | +| `ALTER PAGE` over `--mcp` fails against Studio Pro **11.13** with `pg_patch_page: … PROP_NOT_PRIMITIVE: Property 'widgets' is not a primitive property`. `CREATE PAGE` is fine; the page itself is left intact | 11.13 gave `pg_read_page` a **`depth` argument defaulting to 4**, replacing anything deeper with the literal string `"..."`. ALTER PAGE is read-modify-**replace-whole-page**, so the truncated read went straight back as the new page body. Measured live: `Administration.Account_Overview` read 32,594 bytes at full depth but **1,052 bytes** at the default, its entire tree reduced to `{"widgets":["...","..."]}`. Every ordinary page truncates — three of three PgTest pages did | `mdl/backend/mcp/page.go` (`pgReadPage`, `pgReadFullDepth`, `hasTruncationSentinel`), `mdl/backend/mcp/client.go` (`SupportsToolArg`) | Request the full depth, and **guard rather than trust it**: refuse a read still carrying the sentinel instead of letting a partial page reach a write (ADR-0005 guard-don't-drop). Two traps. (1) **Do not send `depth` unconditionally** — 11.11/11.12 declare `pg_read_page` `additionalProperties:false` without it, so the whole call fails; gate on a live `tools/list` probe of the tool's input schema, because `serverInfo.version` is frozen at `1.0.0` across 11.11/11.12/11.13 and cannot discriminate releases. (2) **Match the sentinel only as an array element** — a caption or title legitimately reading `"..."` is real content, and a naive substring scan rejects valid pages. The release notes announced none of this, exactly as 11.12 silently removed `pg_write_page` (#697): on any Studio Pro upgrade, re-probe `tools/list` and diff the input schemas, not just the tool names. Tests `mdl/backend/mcp/page_depth_test.go`; controls: stub the depth arg (full-depth test fails) and stub the guard (truncation test fails) | diff --git a/.claude/skills/mendix/live-edit-with-studio-pro.md b/.claude/skills/mendix/live-edit-with-studio-pro.md index c882a8158..df3a04fa1 100644 --- a/.claude/skills/mendix/live-edit-with-studio-pro.md +++ b/.claude/skills/mendix/live-edit-with-studio-pro.md @@ -40,21 +40,28 @@ Run a script the same way: `mxcli --mcp http://localhost/mcp --mcp-dial localhos ## What you can change via MCP — check first -**What's authorable over MCP depends on the Studio Pro version**, because the -underlying capability surface grows per release. Before generating MDL for live -editing, ask the connected server what it supports — don't guess: +**What's authorable over MCP depends on the Studio Pro version *and on this +session*.** The capability surface changes per release, but it also depends on +your Studio Pro preferences (some tools are togglable) and on which MCP servers +you have connected to Studio Pro. So the answer is not derivable from a version +number — ask the connected server, every session, before generating MDL: ```bash mxcli mcp capabilities -p /path/to/app.mpr --mcp http://localhost/mcp --mcp-dial localhost:7782 ``` -It prints, for *this* server: what's authorable (modules, entities + ALTER, +It prints, for *this session*: what's authorable (modules, entities + ALTER, associations, enumerations, constants, microflows, pages + ALTER PAGE, workflows, -view entities, documents into folders), what's **not** (e.g. nanoflows, Java -actions, business-event services, security, navigation, MOVE/re-parent, attribute -type change — hard PED limits), and the live tool list. Treat anything reported as -not authorable as off-limits over MCP — do it in Studio Pro or against the on-disk -`.mpr` instead. +navigation, entity access rules, documents into folders), what's **not** (e.g. +nanoflows, Java actions, business-event services, view entities, security roles, +MOVE/re-parent, attribute type change), and the live tool list. Treat anything +reported as not authorable as off-limits over MCP — do it in Studio Pro or against +the on-disk `.mpr` instead. + +A feature can also be reported unavailable because **this session** lacks a tool it +needs, or because the tool probe did not answer; the report says which, and mxcli +fails closed rather than assuming a tool is there. Quote the whole report in a bug +report — a Studio Pro version number alone does not identify the surface you had. New modules and their dependents resolve within the same run, so `create module X; create enumeration X.Status (...)` works in one script. Place a @@ -65,7 +72,11 @@ MOVE can't re-parent over MCP. The machine may run two MCP servers: -- **Studio Pro built-in (port 7782)** — model authoring. **Use this by default.** +- **Studio Pro built-in (port 7782 by default)** — model authoring. **Use this by + default.** From **11.13** Studio Pro **auto-selects a free port** when 7782 is + taken, so multiple instances can run side by side. The active port is shown in + Studio Pro's **status bar** (and set under Preferences > AI > MCP Server) — read + it there rather than assuming 7782, and pass it to `--mcp-dial`. - **Concord (port 7783)** — a temporary gap-filler with operational/refactor tools (`delete_document`, `save_all`, `run_app`, `check_model`). **Only** reach for Concord when the built-in server lacks the capability you need. diff --git a/docs/03-development/PED_MCP_CAPABILITIES.md b/docs/03-development/PED_MCP_CAPABILITIES.md index f3832f1b3..80d8c5c0c 100644 --- a/docs/03-development/PED_MCP_CAPABILITIES.md +++ b/docs/03-development/PED_MCP_CAPABILITIES.md @@ -40,16 +40,25 @@ document is the MCP column's deep-dive. | ≤ 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 | +| 11.13 | Yes | `mendix-studio-pro` 1.0.0 | `2025-06-18` | 2026-08-11 | -> **`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 +> **`serverInfo.version` is frozen at `1.0.0` across 11.11, 11.12 **and 11.13** even +> though the tool surface and behaviour changed in every one of them.** So the server +> version is **not** a reliable discriminator between Studio Pro releases — three +> releases in, treat this as settled rather than provisional. The machine-readable > [`capabilities.yaml`](../../mdl/backend/mcp/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. `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. +> source of truth for the 11.11→11.12 and 11.12→11.13 deltas below. +> +> **Gate a per-release *argument* on a live schema probe, not on any version.** +> `Client.SupportsToolArg(tool, arg)` answers from a cached `tools/list` (which now +> captures each tool's `inputSchema.properties`). Tool schemas are declared +> `additionalProperties:false`, so sending an argument an older server does not know +> fails the whole call — "unknown" must mean "do not send". This is what keeps +> `pg_read_page`'s 11.13-only `depth` off 11.11/11.12 servers. `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 @@ -101,6 +110,65 @@ Tools already present in 11.11 and unchanged (do **not** re-add as "new"): `ped_ **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 `add`ed 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`. +## 11.13 changes (delta vs 11.12) + +Captured live 2026-08-11 (`cmd/mcpprobe -method tools/list`, fixture +`mdl/backend/mcp/testdata/tools-11.13.json`). Tool count stays 18, but the +composition changed. **The 11.13 release notes announce none of the items in this +section** — they cover only auto-port-selection, a status-bar port indicator, and +four fixes to Studio Pro's MCP *client*. This is the second release in a row where +the authoring surface moved silently (11.12 removed `pg_write_page`, #697), so the +live probe — including **input schemas**, not just tool names — is the only +trustworthy source. + +| Change | Tool | Effect on the backend | +|--------|------|-----------------------| +| **Removed** | `oql_generate` | Not used by the backend (LLM-backed; view entities are authored from user OQL verbatim). Its disappearance lines up with 11.13's new **"OQL Generation Toggle"** preference, so treat it as **configuration-dependent, not removed** — see the federation note below. | +| **Added** | `mcp_mendix-marketplace_Component_GetComponentIDsByCriteria` | A **proxied** tool, not a Studio Pro one — see federation below. Not used by the backend. | +| **Changed** | `pg_read_page` | **Breaking — was: ALTER PAGE broken on 11.13.** Gained `depth` (**default 4**) and `paths`; nodes below the limit become the literal string `"..."`. Fixed in `pgReadPage`: request `pgReadFullDepth` when the server advertises the argument, and refuse a read that still carries the sentinel. See the truncation note below. | +| **Changed** | `ped_get_schema` | Gained `kind` (`constructor` \| `element`, default `constructor`), making explicit the two shapes the system prompt always described. **No backend change needed** — `ensureSchema` calls it only to satisfy PED's fetch-before-create contract and discards the body. | +| **Changed** | `ped_update_document` | Description-only. Now states the rule mxcli's `navigation.go` discovered empirically: an element-valued property can be `set` **only while it is null/undefined**; a non-null one cannot be re-set. | +| **Changed** | `ped_create_document` | Description-only (`documentContent`). Its text references a tool named `get_document_schema`, which does not exist in `tools/list` — an upstream naming slip, not a tool we are missing. | + +**Studio Pro now federates the MCP servers it is a client of.** The system prompt +says it outright: *"Capabilities can be extended via MCP (Model Context Protocol) +tools provided by the user. MCP tools are prefixed `mcp_{serverName}_{toolName}`."* +The `mcp_mendix-marketplace_*` entry is the Marketplace MCP server re-exposed +through Studio Pro's own server. Two consequences: the tool surface now varies +**per user configuration** as well as per release, and `tools.listChanged: true` +means it can change within a session. Combined with a togglable `oql_generate`, +**tool presence must be probe-gated, never table-gated** — `capabilities.yaml` can +describe what mxcli does with a tool, but not whether it is there. + +**Observed, and the reason this matters.** The *same* Studio Pro session reported +**18 tools including `mcp_mendix-marketplace_*`, then 17 without it an hour later**, +with no restart (2026-08-11). The federated tool comes and goes with Studio Pro's own +client connection to the Marketplace server — the very thing 11.13's "repeated +disconnect/reconnect" fix addresses. A table asserting that tool's presence would +have been wrong within the hour. The fixture `testdata/tools-11.13.json` captures the +18-tool state; treat its federated entry as a **sample, not a constant**, and do not +write a test that asserts a federated tool is present. + +**Page reads are depth-truncated by default (the 11.13 regression).** Measured on +`Administration.Account_Overview`: 32,594 bytes at full depth, **1,052 bytes** at +the default, the entire widget tree collapsed to +`{"widgets":[{"$Type":"Pages$Content","slot":"Main","widgets":["...","..."]}]}`. +This is not an edge case — all three pages sampled from `PgTest` truncated too. +Because ALTER PAGE is read-modify-**replace-whole-page**, the truncated read went +back as the new page body; `pg_patch_page` **rejected** it (`PROP_NOT_PRIMITIVE: +Property 'widgets' is not a primitive property`) and left the page intact, so the +symptom was a broken ALTER PAGE rather than data loss. `CREATE PAGE` was never +affected (it does not read). The fix has two halves, and the second is the durable +one: `pgReadPage` asks for `pgReadFullDepth`, **and** `hasTruncationSentinel` +refuses any read still carrying a placeholder, so the next change to the server's +truncation default fails loudly instead of silently (ADR-0005 guard-don't-drop). +The sentinel is matched only as an **array element** — a caption or title that +legitimately reads `"..."` is real page content and must not trip the guard. + +**Unchanged gaps, re-verified live on 11.13:** still **no delete-document tool**, +still **no save/flush tool**, reads still expose `$QualifiedName` but **not `$ID`**, +and the security documents are still sealed. + ## Capability gaps (11.11) These are the *absences* that bound what the backend can do. They are as @@ -127,7 +195,24 @@ 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`. +- Otherwise bridge on the **host**. Where `host.docker.internal` resolves to an + **IPv6** address (confirmed on the 11.13 Mac host: `fdc4:f303:9324::254`), a + `TCP4-LISTEN` bridge is not reachable from the container — the listener must + accept both families: + + ```bash + socat TCP6-LISTEN:7790,reuseaddr,fork,ipv6only=0 'TCP6:[::1]:7782' + ``` + + Then dial `host.docker.internal:7790`. Quote the target: zsh glob-expands the + bare `[::1]` and fails with `no matches found`. + +**Finding the port on 11.13+.** Studio Pro now **auto-selects a free port** when the +default is taken, and shows the active one in the **status bar** (Preferences > AI > +MCP Server also configures it). There is no documented file or endpoint exposing it, +so the port is a per-session lookup. A scan is a workable fallback — the server +answers `initialize` with `serverInfo.name: mendix-studio-pro`, which identifies it +unambiguously among other listeners. `cmd/mcpprobe` and the backend client pin the dial target while keeping the `Host` header `localhost` (`-url http://localhost/mcp -dial host.docker.internal:`). @@ -444,6 +529,14 @@ return a clear "not supported by the MCP backend" error via the generated ## Concord (optional second client — gap-filler) +> **Concord is Windows-only — it does not run on macOS** (confirmed 2026-08-11 on +> the 11.13 Mac host). On macOS every Concord-backed capability is simply absent, +> so `DROP` of a standalone document (enumeration, microflow, page) has **no path +> at all**: PED has no delete tool and Concord is the only gap-filler. `check_model` +> is likewise unavailable; `ped_check_errors` remains for per-document validation. +> This also means MCP-authored test documents cannot be cleaned up on macOS — +> remove them in Studio Pro, or close without saving. + 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 @@ -593,7 +686,13 @@ already removes entities/associations). 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: -method tools/list` → save to `mdl/backend/mcp/testdata/tools-.json`. -3. **Diff against the previous `tools.json`** — added/removed/renamed tools. +3. **Diff against the previous `tools.json`** — added/removed/renamed tools, **and + each surviving tool's `inputSchema`**. A name-only diff is not enough: 11.13 + changed no tool name mxcli calls, yet `pg_read_page` gained a `depth` argument + defaulting to 4 that broke ALTER PAGE. Treat a **new argument with a default** + as a behaviour change to the existing call, since the server applies it whether + or not the client knows about it. A new argument mxcli must send has to be gated + with `Client.SupportsToolArg` — never sent unconditionally. 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 diff --git a/docs/13-decisions/0006-mcp-capability-model.md b/docs/13-decisions/0006-mcp-capability-model.md index 63d405808..c1121b88f 100644 --- a/docs/13-decisions/0006-mcp-capability-model.md +++ b/docs/13-decisions/0006-mcp-capability-model.md @@ -2,16 +2,21 @@ - **Status**: Proposed - **Date**: 2026-06-11 +- **Revised**: 2026-08-11 — see [Revision](#revision-2026-08-11). Amended in place + rather than superseded because this ADR is still *Proposed*: the core decision + (probe ∪ table, one source of truth for gate and report) survives intact; only + the **keying** and the **division of labour** between the two halves changed. - **Related**: [PROPOSAL_mcp_backend.md](../11-proposals/PROPOSAL_mcp_backend.md), [`docs/03-development/PED_MCP_CAPABILITIES.md`](../03-development/PED_MCP_CAPABILITIES.md), [ADR-0002](0002-backend-abstraction.md) ## Context The MCP backend authors model changes through Studio Pro's embedded MCP server -("PED"). That server's authoring surface **grows with every Studio Pro version** — -new tools appear (a delete tool, a save tool), and the set of document types -`ped_create_document` accepts (its "create whitelist") expands. So what the MCP -backend can do is `f(Mendix version) ∩ f(PED capabilities)`, where the second term -moves per release. +("PED"). That server's authoring surface **changes with every Studio Pro version** — +new tools appear (a delete tool, a save tool) and the set of document types +`ped_create_document` accepts (its "create whitelist") expands, but tools are also +**removed** (11.12 dropped `pg_write_page`, 11.13 dropped `oql_generate`) and +existing tools change shape. So what the MCP backend can do is +`f(Mendix version) ∩ f(PED capabilities)`, where the second term moves per release. Two problems follow: @@ -35,9 +40,11 @@ is in `tools/list`, but the create-whitelist is in no schema — we learned it o ## Decision Model PED authoring capability as a **single source of truth computed on connect**: -the union of a live `tools/list` probe (tool-presence capabilities) and a maintained -**version-keyed capability table** (the create-whitelist and behavioral quirks that -are not schema-discoverable, keyed by MCP `serverInfo.version` / Studio Pro version). +the union of a live `tools/list` probe and a maintained capability table, split by +what each can actually answer. **Everything observable in `tools/list` — tool +presence and tool input schemas — is probe-only; the table never asserts it.** The +table carries only the non-discoverable facts (the create-whitelist, behavioural +quirks), **keyed on the project's Mendix version**, never on `serverInfo.version`. The backend gates all authoring decisions on this model, and the agent-facing capability report is generated from the same model — so behavior and report cannot diverge. @@ -60,6 +67,17 @@ diverge. - **(neutral)** `PED_MCP_CAPABILITIES.md` shifts from being the authority to being the human-readable narrative *over* the machine table (kept consistent by the onboarding step, or generated from the table). +- **(+) The report stops lying about a togglable tool.** Gating presence on the live + probe means a tool the user has switched off reads as absent, which is the truth + for that session. +- **(−) Capability now depends on session state, not just versions.** Two runs against + the same Studio Pro can report different capabilities if a preference changed or an + MCP server was connected. That is a faithful model of the system rather than a + regression, but it means a capability report is only valid for the session that + produced it, and bug reports must carry it rather than just a version number. +- **(−) The probe becomes load-bearing.** If `tools/list` fails, presence is unknown; + the gates must fail closed (treat as absent) rather than assume. That trades a + false "yes" for a false "no", which is the safe direction for a write path. ## Alternatives considered @@ -73,3 +91,49 @@ diverge. gives the agent no report — the status quo this ADR exists to replace. - **Static per-version capability docs only.** Drifts from behavior and isn't machine-consumable by either the backend or the agent. +- **Key the table on `serverInfo.version`** (the original form of this ADR). + Rejected on evidence — see the Revision below. + +## Revision (2026-08-11) + +Onboarding Studio Pro 11.13 falsified two assumptions in the original decision (1 +and 2 below) and widened the scope of a third (3). All were reasonable when +written; none survived contact with three releases. + +**1. `serverInfo.version` cannot key anything.** It has read `1.0.0` for 11.11, +11.12 *and* 11.13, while the tool surface changed in every one. The `available_since` +mechanism this ADR designed as its escape hatch ("flip one entry and the feature +lights up") is therefore not merely unused but **structurally dead**: it resolves +through `serverVersionAtLeast(b.server.Version, want)`, which for a frozen `1.0.0` +is false for every `want` above it. No entry uses it today, which is why the defect +went unnoticed. The replacement axis is the **project's Mendix version**, already +the precedent in `gateAttributeDefaults` (`ProjectVersion().IsAtLeast(11,12)`). + +**2. The table must not assert tool presence.** The original framing treated presence +as static per version. 11.13 shows it is neither static nor version-derived: + +- **Tools are user-togglable.** `oql_generate` disappears when the new "OQL + Generation" preference is off. A table asserting it is present would be wrong for + a supported configuration, not merely stale. +- **Studio Pro federates other MCP servers into its own surface.** Its system prompt + states it: *"Capabilities can be extended via MCP tools provided by the user. MCP + tools are prefixed `mcp_{serverName}_{toolName}`."* An `mcp_mendix-marketplace_*` + tool now appears in `tools/list`. So the surface varies per **user configuration**, + which no version table can model. +- **`tools.listChanged: true`** means it can change *within* a session. + +Federated `mcp_*` tools are reported but never gated on: they are third-party tools +whose contract mxcli does not control, and a capability mxcli claims must be one it +can guarantee. + +**3. Probing extends to input schemas, not just names.** 11.13 renamed nothing mxcli +calls, yet `pg_read_page` gained a `depth` argument defaulting to 4 that broke ALTER +PAGE — a behaviour change delivered entirely through an existing tool's schema, and +applied by the server whether or not the client knows about it. `Client.SupportsToolArg` +(shipped with that fix) is the general form: an argument mxcli must send is gated on +the live schema, defaulting to *not sent*, because tool schemas are +`additionalProperties:false` and an unknown argument fails the whole call. + +The net effect on the split: the probe half **grows** (presence + schemas, and it +becomes the gate rather than decoration), and the table half **shrinks** to what is +genuinely unobservable, re-keyed on the project's Mendix version. diff --git a/mdl/backend/mcp/backend.go b/mdl/backend/mcp/backend.go index 2c50e86d1..7b3c1e88b 100644 --- a/mdl/backend/mcp/backend.go +++ b/mdl/backend/mcp/backend.go @@ -63,6 +63,11 @@ type Backend struct { // this session (the contract asks for a schema fetch before create/add). schemaFetched map[string]bool + // capsCache memoizes the session's resolved capability set. Every authoring + // gate consults it and resolution costs a tools/list round-trip, so it is + // computed once per connection. + capsCache *Capabilities + // dirty holds module names whose live (in-memory) domain model has diverged // from the on-disk .mpr because of writes this session. Reads of a dirty // module are reconstructed from MCP instead of the stale local reader — diff --git a/mdl/backend/mcp/capabilities.go b/mdl/backend/mcp/capabilities.go index 8ef7babbc..df00c2805 100644 --- a/mdl/backend/mcp/capabilities.go +++ b/mdl/backend/mcp/capabilities.go @@ -10,6 +10,8 @@ import ( "strings" "gopkg.in/yaml.v3" + + "github.com/mendixlabs/mxcli/mdl/types" ) // capabilities.yaml is the version-keyed table half of the capability model @@ -27,59 +29,118 @@ const ( capViewEntityCreate = "view_entities" ) -// Capability is one authorable/blocked feature for a given server version. +// Capability is one authorable/blocked feature, resolved for the connected +// session (project Mendix version + live tool probe). type Capability struct { Key string Feature string Available bool Note string + // Blocker, when non-empty, says why an otherwise-available feature is off + // for *this* session (a missing tool, or a probe that could not run). It is + // session state, not a property of the version — see ADR-0006's Revision. + Blocker string } // Capabilities is the effective capability set for a connected server: the -// version-keyed table merged with the live server identity and tool probe. The -// agent-facing report and (in slice 3) the backend's authoring gates read from it, -// so they cannot drift. +// table (keyed on the project's Mendix version) merged with the live server +// identity and tool probe. The agent-facing report and the backend's authoring +// gates read from it, so they cannot drift. +// +// It is valid only for the session that produced it: tool presence varies with +// the user's Studio Pro preferences and configured MCP servers, not just with +// versions (ADR-0006 Revision). type Capabilities struct { ServerName string ServerVersion string + ProjectVersion string ConcordConnected bool - Tools []string - Features []Capability + // Tools are the Studio Pro tools present, from the live probe. + Tools []string + // FederatedTools are tools Studio Pro proxies from MCP servers the user has + // connected to it (prefixed mcp__). Reported for visibility, + // never gated on: mxcli does not control their contract. + FederatedTools []string + // ToolsProbed records whether tools/list actually answered. False means tool + // presence is unknown, and tool-dependent features fail closed. + ToolsProbed bool + Features []Capability } +// federatedToolPrefix marks a tool Studio Pro proxies from another MCP server. +// Studio Pro's system prompt: "MCP tools are prefixed mcp_{serverName}_{toolName}". +const federatedToolPrefix = "mcp_" + type capabilityTable struct { - BaselineServerVersion string `yaml:"baseline_server_version"` - Features []struct { - Key string `yaml:"key"` - Feature string `yaml:"feature"` - Available bool `yaml:"available"` - AvailableSince string `yaml:"available_since"` - Note string `yaml:"note"` + Features []struct { + Key string `yaml:"key"` + Feature string `yaml:"feature"` + Available bool `yaml:"available"` + // AvailableSinceMendix is the project's Mendix version ("11.12") from + // which this feature is authorable. Keyed on the *project* version + // because the MCP serverInfo.version is frozen at 1.0.0 across releases + // and cannot discriminate them (ADR-0006 Revision). + AvailableSinceMendix string `yaml:"available_since_mendix"` + // RequiresTools are the Studio Pro tools the feature needs. Which tools a + // feature depends on is not observable, so it lives here; whether they are + // present is answered only by the live probe. + RequiresTools []string `yaml:"requires_tools"` + Note string `yaml:"note"` } `yaml:"features"` } -// pedCapabilityFeatures resolves the feature capabilities for a connected MCP -// server version. A feature blocked at baseline becomes available once the server -// reaches its `available_since` — so lifting a PED limit is a one-line table edit. -func pedCapabilityFeatures(serverVersion string) []Capability { +// loadCapabilityTable returns the embedded table. Embedded + validated by +// TestCapabilityTableParses; a parse failure would be a build-time content bug, +// so degrade to empty rather than panic. +func loadCapabilityTable() capabilityTable { var t capabilityTable - // Embedded + validated by TestCapabilityTableParses; a parse failure here would - // be a build-time content bug, so degrade to empty rather than panic. _ = yaml.Unmarshal(capabilityTableYAML, &t) + return t +} + +// resolveCapabilities computes the effective feature set for a session. +// +// Three inputs, in order: the table's baseline, the project's Mendix version +// (which can turn a baseline-blocked feature on), and the live tool probe (which +// can turn any feature off). The probe only ever subtracts — a feature mxcli has +// no create path for does not become available because a tool appeared. +func resolveCapabilities(pv *types.ProjectVersion, tools []string, probed bool) []Capability { + present := make(map[string]bool, len(tools)) + for _, t := range tools { + present[t] = true + } + t := loadCapabilityTable() out := make([]Capability, 0, len(t.Features)) for _, f := range t.Features { - available := f.Available - if !available && f.AvailableSince != "" && serverVersionAtLeast(serverVersion, f.AvailableSince) { - available = true + c := Capability{Key: f.Key, Feature: f.Feature, Available: f.Available, Note: f.Note} + if !c.Available && f.AvailableSinceMendix != "" && projectVersionAtLeast(pv, f.AvailableSinceMendix) { + c.Available = true + } + // Fail closed: an unavailable tool, or an unknown tool surface, blocks a + // feature that needs it. A false "no" is the safe direction for a write + // path — the alternative is failing mid-write against a missing tool. + if c.Available && len(f.RequiresTools) > 0 { + switch { + case !probed: + c.Available, c.Blocker = false, "tool probe (tools/list) failed, so tool presence is unknown" + default: + for _, need := range f.RequiresTools { + if !present[need] { + c.Available = false + c.Blocker = fmt.Sprintf("Studio Pro does not expose the %q tool in this session", need) + break + } + } + } } - out = append(out, Capability{Key: f.Key, Feature: f.Feature, Available: available, Note: f.Note}) + out = append(out, c) } return out } -// capability looks up a capability by key for the connected server version. +// capability looks up a capability by key, resolved for the connected session. func (b *Backend) capability(key string) (Capability, bool) { - for _, c := range pedCapabilityFeatures(b.server.Version) { + for _, c := range b.capabilities().Features { if c.Key == key { return c, true } @@ -97,11 +158,18 @@ func (b *Backend) canAuthor(key string) bool { } // notAuthorable builds the rejection for a blocked capability, sourcing the reason -// from the table (the message is single-source too, not a hardcoded string). +// from the table (the message is single-source too, not a hardcoded string). A +// session-specific Blocker wins over the table note, because "this Studio Pro +// session does not expose the tool" is more actionable than the generic limit. func (b *Backend) notAuthorable(kind, name, key string) error { note := "not supported by this Studio Pro version over MCP" - if c, ok := b.capability(key); ok && c.Note != "" { - note = c.Note + if c, ok := b.capability(key); ok { + switch { + case c.Blocker != "": + note = c.Blocker + case c.Note != "": + note = c.Note + } } return fmt.Errorf("%s %q is not authorable via the MCP backend — %s; create it against a local .mpr or in Studio Pro", kind, name, note) } @@ -113,21 +181,52 @@ func errCreatePathUnbuilt(kind, name string) error { return fmt.Errorf("%s %q: the capability table marks this authorable, but the MCP backend's create path for it is not implemented — build the path before flipping the table", kind, name) } -// capabilities builds the effective capability set: the version-keyed table for the -// connected server version, plus live identity/Concord/tools. +// capabilities builds the effective capability set: the table resolved against +// the project's Mendix version, narrowed by the live tool probe, plus live +// identity/Concord. +// +// Cached for the session: every authoring gate calls this, and the probe is a +// network round-trip. tools.listChanged means the surface *can* move mid-session, +// but re-probing per gate would cost a round-trip on every write for a change +// mxcli has no way to act on mid-statement. func (b *Backend) capabilities() Capabilities { + if b.capsCache != nil { + return *b.capsCache + } caps := Capabilities{ ServerName: b.server.Name, ServerVersion: b.server.Version, ConcordConnected: b.concord != nil, - Features: pedCapabilityFeatures(b.server.Version), + } + var pv *types.ProjectVersion + if b.reader != nil { + pv = b.ProjectVersion() + if pv != nil { + caps.ProjectVersion = pv.String() + } } if b.client != nil { if tools, err := b.client.ListTools(); err == nil { - sort.Strings(tools) - caps.Tools = tools + caps.ToolsProbed = true + for _, t := range tools { + if strings.HasPrefix(t, federatedToolPrefix) { + caps.FederatedTools = append(caps.FederatedTools, t) + continue + } + caps.Tools = append(caps.Tools, t) + } + sort.Strings(caps.Tools) + sort.Strings(caps.FederatedTools) } } + // Only Studio Pro's own tools gate capability; federated ones are third-party + // and mxcli does not control their contract (ADR-0006 Revision). + caps.Features = resolveCapabilities(pv, caps.Tools, caps.ToolsProbed) + // Memoize only a connected session. Caching a pre-Connect call would pin an + // empty tool surface for the rest of the run, blocking every gated feature. + if b.client != nil { + b.capsCache = &caps + } return caps } @@ -141,6 +240,7 @@ func (b *Backend) CapabilityReport() string { sb.WriteString("MCP backend capabilities\n") sb.WriteString("========================\n") fmt.Fprintf(&sb, "Studio Pro MCP server : %s %s\n", orUnknown(caps.ServerName), orUnknown(caps.ServerVersion)) + fmt.Fprintf(&sb, "Project Mendix version: %s\n", orUnknown(caps.ProjectVersion)) concord := "not connected — DROP of standalone docs (enum/microflow/page/…) is unavailable" if caps.ConcordConnected { concord = "connected" @@ -153,17 +253,32 @@ func (b *Backend) CapabilityReport() string { fmt.Fprintf(&sb, " ✓ %s — %s\n", c.Feature, c.Note) } } - sb.WriteString("\nNot authorable (PED limits this version):\n") + sb.WriteString("\nNot authorable:\n") for _, c := range caps.Features { if !c.Available { - fmt.Fprintf(&sb, " ✗ %s — %s\n", c.Feature, c.Note) + reason := c.Note + if c.Blocker != "" { + reason = c.Blocker + } + fmt.Fprintf(&sb, " ✗ %s — %s\n", c.Feature, reason) } } sb.WriteString("\nReads (SHOW / DESCRIBE of any document type): always available from the local .mpr.\n") - if len(caps.Tools) > 0 { - fmt.Fprintf(&sb, "\nPED tools present (%d): %s\n", len(caps.Tools), strings.Join(caps.Tools, ", ")) + if !caps.ToolsProbed { + sb.WriteString("\n⚠ tools/list did not answer, so tool presence is unknown; tool-dependent\n" + + " features are reported unavailable rather than assumed present.\n") + } else { + fmt.Fprintf(&sb, "\nStudio Pro tools present (%d): %s\n", len(caps.Tools), strings.Join(caps.Tools, ", ")) } + if len(caps.FederatedTools) > 0 { + fmt.Fprintf(&sb, "\nFederated tools (%d), proxied by Studio Pro from MCP servers you connected to it.\n"+ + "mxcli reports these but never relies on them — their contract is not mxcli's to guarantee:\n %s\n", + len(caps.FederatedTools), strings.Join(caps.FederatedTools, ", ")) + } + sb.WriteString("\nThis report describes THIS session. Tool presence varies with your Studio Pro\n" + + "preferences and connected MCP servers, not only with versions — quote it in bug\n" + + "reports rather than a version number alone.\n") sb.WriteString("\nDetail & per-version onboarding: docs/03-development/PED_MCP_CAPABILITIES.md\n") return sb.String() } @@ -175,31 +290,40 @@ func orUnknown(s string) string { return s } -// serverVersionAtLeast reports whether have >= want for dotted numeric versions -// (e.g. "1.2.0" >= "1.1.0"). Non-numeric segments compare as 0. -func serverVersionAtLeast(have, want string) bool { - h, w := splitVersion(have), splitVersion(want) - for i := 0; i < len(h) || i < len(w); i++ { - var hv, wv int - if i < len(h) { - hv = h[i] - } - if i < len(w) { - wv = w[i] - } - if hv != wv { - return hv > wv - } +// projectVersionAtLeast reports whether the project's Mendix version is at least +// want ("11.12"). A nil project version (no local reader) reports false, so a +// version-gated feature stays off rather than being assumed available. +// +// This replaces the previous serverVersionAtLeast gate, which was dead code: it +// compared MCP serverInfo.version, frozen at 1.0.0 across 11.11/11.12/11.13, so +// no `available_since` above the baseline could ever resolve true (ADR-0006 +// Revision). +func projectVersionAtLeast(pv *types.ProjectVersion, want string) bool { + if pv == nil { + return false + } + major, minor, ok := splitMajorMinor(want) + if !ok { + return false } - return true // equal + return pv.IsAtLeast(major, minor) } -func splitVersion(v string) []int { +// splitMajorMinor parses "11.12" / "11.12.0" into (11, 12). It reports ok=false +// for anything it cannot parse, so a malformed table entry blocks the feature +// instead of silently gating on zero (which would make it always available). +func splitMajorMinor(v string) (major, minor int, ok bool) { parts := strings.Split(v, ".") - out := make([]int, len(parts)) - for i, p := range parts { - n, _ := strconv.Atoi(strings.TrimFunc(p, func(r rune) bool { return r < '0' || r > '9' })) - out[i] = n + if len(parts) < 2 { + return 0, 0, false } - return out + major, err := strconv.Atoi(strings.TrimSpace(parts[0])) + if err != nil { + return 0, 0, false + } + minor, err = strconv.Atoi(strings.TrimSpace(parts[1])) + if err != nil { + return 0, 0, false + } + return major, minor, true } diff --git a/mdl/backend/mcp/capabilities.yaml b/mdl/backend/mcp/capabilities.yaml index 9f6ce5dc6..faf805ee5 100644 --- a/mdl/backend/mcp/capabilities.yaml +++ b/mdl/backend/mcp/capabilities.yaml @@ -1,54 +1,78 @@ -# PED (Studio Pro MCP server) authoring capabilities — the version-keyed table half -# of the capability model (ADR-0004). Tool *presence* is NOT here; it comes from a -# live tools/list probe. This file holds the facts that are not schema-discoverable: -# which features are authorable, and the reasons the rest are not (PED's create -# whitelist, behavioral quirks). +# PED (Studio Pro MCP server) authoring capabilities — the table half of the +# capability model (ADR-0006). This file holds ONLY what a live probe cannot +# answer: which features mxcli can author, and why the rest are blocked (PED's +# create whitelist, behavioural quirks). # -# Keyed by MCP serverInfo.version. The baseline below is the first server (1.0.0, -# shipped in Studio Pro 11.11). `available` means *the MCP backend can author it* — -# both PED permits it AND mxcli has a create path. To lift a blocked feature, set -# `available_since` to the MCP server version once BOTH hold (the loader flips it for -# servers at or above that version, and both the agent report and the backend's gate -# pick it up — they read this same table via `key`). Auditing entries is part of the -# "onboarding a new version" procedure in docs/03-development/PED_MCP_CAPABILITIES.md. +# Tool *presence* is NOT here and must never be added. Presence comes from the +# live tools/list probe, because it varies with the user's Studio Pro preferences +# (a togglable tool like oql_generate) and with the MCP servers they have +# connected to Studio Pro — neither of which any version table can model. +# See ADR-0006's Revision (2026-08-11). # -# `key` is the stable machine identifier the backend gates on (canAuthor(key)); the -# capability constants in capabilities.go must match these. - -baseline_server_version: "1.0.0" +# Fields: +# key stable machine id the backend gates on (canAuthor(key)); +# the constants in capabilities.go must match these. +# available authorable at baseline (Studio Pro 11.11 / the first MCP +# server). True means BOTH PED permits it AND mxcli has a +# create path — never flip it for one without the other. +# available_since_mendix the PROJECT's Mendix version from which it is authorable +# ("11.12"). Keyed on the project version because MCP +# serverInfo.version is frozen at 1.0.0 across 11.11/11.12/ +# 11.13 and cannot discriminate releases. +# requires_tools Studio Pro tools the feature needs. WHICH tools a feature +# depends on is not observable, so it belongs here; WHETHER +# they are present is answered only by the probe. A missing +# tool (or a probe that did not answer) fails the feature +# closed. +# note the reason, surfaced verbatim in the agent report and in +# the backend's rejection message. +# +# Auditing entries is part of the "onboarding a new version" procedure in +# docs/03-development/PED_MCP_CAPABILITIES.md. features: - key: modules feature: "Modules" available: true + requires_tools: [ped_create_module] note: "CREATE" - key: entities feature: "Entities" available: true + requires_tools: [ped_update_document] note: "CREATE/DROP (+ NOT NULL / UNIQUE validation rules on create); ALTER add/drop/rename attribute, entity & attribute documentation; attribute default values (Studio Pro 11.12+, via the value/defaultValue path-op); generalization (extends)" - key: associations feature: "Associations" available: true + requires_tools: [ped_update_document] note: "CREATE/DROP within a module" - key: enumerations feature: "Enumerations" available: true + requires_tools: [ped_create_document] note: "CREATE (DROP via Concord)" - key: constants feature: "Constants" available: true + requires_tools: [ped_update_document] note: "CREATE / CREATE OR MODIFY / DROP (String/Integer/Decimal/Boolean/DateTime)" - key: microflows feature: "Microflows" available: true + requires_tools: [ped_update_document] note: "CREATE (broad activity + control-flow coverage)" - key: pages feature: "Pages" available: true + # Pages are the one document type PED is forbidden for; they have their own + # tool pair. 11.12 replaced pg_write_page with pg_patch_page, so a session + # exposing neither cannot author pages at all. + requires_tools: [pg_read_page, pg_patch_page] note: "CREATE + ALTER (widget coverage grows per type); ALTER page-level SET Title (maps onto the pg LightPage's top-level title). Url and pop-up settings (PopupWidth/Height/Resizable/CloseAction) are NOT on the pg LightPage shape (schema is additionalProperties:false) — set them in Studio Pro" - key: workflows feature: "Workflows" available: true + requires_tools: [ped_update_document] note: "CREATE / CREATE OR REPLACE / DROP / ALTER (full, any nesting depth)" - key: view_entities feature: "View entities — CREATE" @@ -57,6 +81,7 @@ features: - key: documents_into_folders feature: "Documents into folders" available: true + requires_tools: [ped_create_document] note: "create … folder 'A/B' (nested; folder auto-created)" # Gated create rejections — the backend checks these keys via canAuthor(). @@ -78,10 +103,12 @@ features: - key: navigation feature: "Navigation (web profile: home page, login/not-found page, menu tree)" available: true + requires_tools: [ped_update_document] note: "CREATE OR REPLACE NAVIGATION on a web profile, via generic ped_update_document path-ops on the project-level Navigation$NavigationDocument (menu items use Menus$MenuItem + Pages$*ClientAction; the menu is cleared then rebuilt). Native profiles (bottom-bar items, native home) and role-based home pages are not authored yet" - key: entity_access_rules feature: "Entity access rules (GRANT)" available: true + requires_tools: [ped_update_document] note: "GRANT a NEW entity access rule for an existing role — the rule lives on DomainModels$Entity.accessRules (the domain-model document PED authors), NOT the security document. ADD-only: PED refuses to remove DomainModels$AccessRule / DomainModels$MemberAccess, so REVOKE and replacing an existing rule in place are rejected (edit in Studio Pro). The referenced module role must already exist" - key: security_roles feature: "Security roles & settings — module roles, user roles, demo users, project security settings" diff --git a/mdl/backend/mcp/capabilities_test.go b/mdl/backend/mcp/capabilities_test.go index e8dc38c59..fb6327764 100644 --- a/mdl/backend/mcp/capabilities_test.go +++ b/mdl/backend/mcp/capabilities_test.go @@ -5,10 +5,23 @@ package mcp import ( "strings" "testing" + + "github.com/mendixlabs/mxcli/mdl/types" ) +// allTools is the Studio Pro tool surface every table entry depends on, so a +// test that is not about tool presence gets a fully-equipped session. +var allTools = []string{ + "ped_create_document", "ped_create_module", "ped_update_document", + "pg_read_page", "pg_patch_page", +} + +func mendix(major, minor int) *types.ProjectVersion { + return &types.ProjectVersion{MajorVersion: major, MinorVersion: minor} +} + func TestCapabilityTableParses(t *testing.T) { - feats := pedCapabilityFeatures("1.0.0") + feats := resolveCapabilities(mendix(11, 11), allTools, true) if len(feats) == 0 { t.Fatal("embedded capability table parsed to zero features") } @@ -34,28 +47,81 @@ func TestCapabilityTableParses(t *testing.T) { } } -func TestServerVersionAtLeast(t *testing.T) { +// A missing tool must switch a feature off for the session, and say which tool. +// The table can no longer assert presence: 11.13 made the surface depend on the +// user's Studio Pro preferences and connected MCP servers (ADR-0006 Revision). +func TestResolveCapabilities_MissingToolBlocksFeature(t *testing.T) { + var withoutPatch []string + for _, tool := range allTools { + if tool != "pg_patch_page" { + withoutPatch = append(withoutPatch, tool) + } + } + byKey := map[string]Capability{} + for _, c := range resolveCapabilities(mendix(11, 13), withoutPatch, true) { + byKey[c.Key] = c + } + pages := byKey["pages"] + if pages.Available { + t.Fatal("pages must be unavailable when pg_patch_page is absent") + } + if !strings.Contains(pages.Blocker, "pg_patch_page") { + t.Fatalf("blocker should name the missing tool, got %q", pages.Blocker) + } + // Features that do not need that tool are untouched. + if !byKey["entities"].Available { + t.Error("entities must stay available; it does not depend on pg_patch_page") + } +} + +// A probe that did not answer means presence is unknown. Fail closed: a false +// "no" is the safe direction for a write path. +func TestResolveCapabilities_FailsClosedWhenProbeFailed(t *testing.T) { + byKey := map[string]Capability{} + for _, c := range resolveCapabilities(mendix(11, 13), nil, false) { + byKey[c.Key] = c + } + if byKey["entities"].Available { + t.Fatal("tool-dependent features must fail closed when tools/list did not answer") + } + if !strings.Contains(byKey["entities"].Blocker, "probe") { + t.Fatalf("blocker should explain the probe failure, got %q", byKey["entities"].Blocker) + } +} + +// The version axis is the PROJECT's Mendix version. Keying on the MCP +// serverInfo.version was dead code — it is frozen at 1.0.0 across 11.11/11.12/ +// 11.13, so no entry above the baseline could ever resolve true. +func TestProjectVersionAtLeast(t *testing.T) { cases := []struct { - have, want string - ge bool + name string + pv *types.ProjectVersion + want string + ge bool }{ - {"1.0.0", "1.0.0", true}, - {"1.2.0", "1.1.0", true}, - {"1.0.0", "1.1.0", false}, - {"2.0.0", "1.9.9", true}, - {"1.0", "1.0.1", false}, + {"equal", mendix(11, 12), "11.12", true}, + {"newer minor", mendix(11, 13), "11.12", true}, + {"older minor", mendix(11, 11), "11.12", false}, + {"newer major", mendix(12, 0), "11.99", true}, + {"nil version stays blocked", nil, "11.12", false}, + {"unparseable entry blocks rather than gating on zero", mendix(11, 13), "eleven", false}, + {"single segment blocks", mendix(11, 13), "11", false}, } for _, c := range cases { - if got := serverVersionAtLeast(c.have, c.want); got != c.ge { - t.Errorf("serverVersionAtLeast(%q,%q) = %v, want %v", c.have, c.want, got, c.ge) - } + t.Run(c.name, func(t *testing.T) { + if got := projectVersionAtLeast(c.pv, c.want); got != c.ge { + t.Errorf("projectVersionAtLeast(%v, %q) = %v, want %v", c.pv, c.want, got, c.ge) + } + }) } } func TestCanAuthorAndNotAuthorable(t *testing.T) { - b := &Backend{} // no connection -> baseline table (server version "") + b := &Backend{capsCache: &Capabilities{ + Features: resolveCapabilities(mendix(11, 13), allTools, true), + }} if !b.canAuthor("entities") { - t.Error("entities should be authorable at baseline") + t.Error("entities should be authorable with the full tool surface") } if b.canAuthor(capNanoflowCreate) { t.Error("nanoflow_create should be blocked at baseline") @@ -70,21 +136,57 @@ func TestCanAuthorAndNotAuthorable(t *testing.T) { } } +// A session-specific blocker is more actionable than the generic table note, so +// it wins in the rejection message. +func TestNotAuthorable_PrefersSessionBlocker(t *testing.T) { + b := &Backend{capsCache: &Capabilities{ + Features: resolveCapabilities(mendix(11, 13), nil, false), + }} + err := b.notAuthorable("page", "P", "pages") + if err == nil || !strings.Contains(err.Error(), "probe") { + t.Errorf("rejection should cite the session blocker, got %v", err) + } +} + func TestCapabilityReport(t *testing.T) { - r := (&Backend{}).CapabilityReport() + b := &Backend{capsCache: &Capabilities{ + ProjectVersion: "11.13.0", + ToolsProbed: true, + Tools: allTools, + FederatedTools: []string{"mcp_mendix-marketplace_Component_GetComponentIDsByCriteria"}, + Features: resolveCapabilities(mendix(11, 13), allTools, true), + }} + r := b.CapabilityReport() for _, want := range []string{ "MCP backend capabilities", - "Studio Pro MCP server : (unknown) (unknown)", + "Project Mendix version: 11.13.0", "✓ Workflows —", // authorable, from table - "✗ Nanoflows — CREATE", // blocked, from a now-split keyed entry - "Reads (SHOW / DESCRIBE", + "✗ Nanoflows — CREATE", // blocked, from a keyed entry + "Studio Pro tools present", + "Federated tools (1)", + "never relies on them", + "describes THIS session", "PED_MCP_CAPABILITIES.md", } { if !strings.Contains(r, want) { t.Errorf("capability report missing %q in:\n%s", want, r) } } - if strings.Contains(r, "PED tools present") { - t.Error("no live client -> should not print a tool list") + // A federated tool must not be counted among Studio Pro's own. + if strings.Contains(r, "Studio Pro tools present (6)") { + t.Error("federated tools must not inflate the Studio Pro tool count") + } +} + +func TestCapabilityReport_WarnsWhenProbeFailed(t *testing.T) { + b := &Backend{capsCache: &Capabilities{ + Features: resolveCapabilities(nil, nil, false), + }} + r := b.CapabilityReport() + if !strings.Contains(r, "tools/list did not answer") { + t.Errorf("report must flag an unknown tool surface, got:\n%s", r) + } + if strings.Contains(r, "Studio Pro tools present") { + t.Error("must not print a tool list when the probe did not answer") } } diff --git a/mdl/backend/mcp/client.go b/mdl/backend/mcp/client.go index 2d727d0a4..0c6e79c87 100644 --- a/mdl/backend/mcp/client.go +++ b/mdl/backend/mcp/client.go @@ -38,6 +38,12 @@ type Client struct { // trace, when set, reports each CallTool invocation for --mcp-verbose / // --mcp-trace. nil is a no-op (Tracer methods guard their receiver). trace *backend.Tracer + + // toolArgs caches the input-schema property names each tool advertises, + // keyed by tool name. Populated lazily by the first SupportsToolArg call. + // nil until probed; an empty (non-nil) map records a failed/empty probe so + // we do not re-probe on every call. + toolArgs map[string]map[string]bool } // ClientOptions configures a Client. @@ -232,19 +238,51 @@ func (c *Client) ListTools() ([]string, error) { } var r struct { Tools []struct { - Name string `json:"name"` + Name string `json:"name"` + InputSchema struct { + Properties map[string]json.RawMessage `json:"properties"` + } `json:"inputSchema"` } `json:"tools"` } if err := json.Unmarshal(res.Result, &r); err != nil { return nil, fmt.Errorf("decode tools/list: %w", err) } names := make([]string, 0, len(r.Tools)) + args := make(map[string]map[string]bool, len(r.Tools)) for _, t := range r.Tools { names = append(names, t.Name) + props := make(map[string]bool, len(t.InputSchema.Properties)) + for p := range t.InputSchema.Properties { + props[p] = true + } + args[t.Name] = props } + c.toolArgs = args return names, nil } +// SupportsToolArg reports whether the connected server advertises an input +// property named arg on the given tool. It answers from a tools/list probe +// (cached for the session), because the tool schemas vary by Studio Pro release +// while serverInfo.version stays frozen at 1.0.0 and so cannot discriminate +// them — see docs/03-development/PED_MCP_CAPABILITIES.md. +// +// It reports false when the probe fails or the tool is absent. That is the safe +// default: every argument this gates is one older servers reject outright +// (their schemas are additionalProperties:false), so "unknown" must mean "do +// not send". Callers must stay correct when it returns false. +func (c *Client) SupportsToolArg(tool, arg string) bool { + if c.toolArgs == nil { + if _, err := c.ListTools(); err != nil { + // Record the failure so a broken/unsupported probe is not retried + // on every subsequent call. + c.toolArgs = map[string]map[string]bool{} + return false + } + } + return c.toolArgs[tool][arg] +} + func (c *Client) CallTool(name string, arguments any) (*ToolResult, error) { if c.trace.Enabled() { target, detail := summarizeToolCall(name, arguments) diff --git a/mdl/backend/mcp/client_test.go b/mdl/backend/mcp/client_test.go index ffa2fb651..39d3c245f 100644 --- a/mdl/backend/mcp/client_test.go +++ b/mdl/backend/mcp/client_test.go @@ -21,6 +21,10 @@ type fakePED struct { // JSON-RPC error object (e.g. Studio Pro's -32000 "Request timed out") // instead of a tool result. rpcErr func(name string, args map[string]any) (code int, msg string, ok bool) + // tools, when set, is the surface reported by tools/list: tool name → the + // input-schema property names it advertises. Drives SupportsToolArg, which + // gates per-release arguments such as pg_read_page's 11.13 `depth`. + tools map[string][]string } type recordedCall struct { @@ -55,6 +59,22 @@ func newFakePED(t *testing.T, respond func(name string, args map[string]any) (st "protocolVersion": "2025-06-18", "serverInfo": map[string]any{"name": "fake-studio-pro", "version": "1.0.0"}, } + case "tools/list": + list := make([]map[string]any, 0, len(f.tools)) + for name, props := range f.tools { + properties := map[string]any{} + for _, p := range props { + properties[p] = map[string]any{"type": "string"} + } + list = append(list, map[string]any{ + "name": name, + "inputSchema": map[string]any{ + "type": "object", "additionalProperties": false, + "properties": properties, + }, + }) + } + result = map[string]any{"tools": list} case "tools/call": var p struct { Name string `json:"name"` diff --git a/mdl/backend/mcp/page.go b/mdl/backend/mcp/page.go index a56452799..45f2630cf 100644 --- a/mdl/backend/mcp/page.go +++ b/mdl/backend/mcp/page.go @@ -172,14 +172,36 @@ func pageParameters(params []*pages.PageParameter) []any { return out } +// pgTruncationSentinel is the placeholder Studio Pro substitutes for a node +// deeper than pg_read_page's depth limit. +const pgTruncationSentinel = "..." + +// pgReadFullDepth is the depth requested so pg_read_page returns a page whole. +// +// Studio Pro 11.13 added a `depth` argument that defaults to 4 and replaces +// deeper nodes with pgTruncationSentinel. That default is far shallower than a +// real page: an ordinary Atlas page read at depth 4 collapses to +// {"widgets":[{"$Type":"Pages$Content","slot":"Main","widgets":["...","..."]}]} +// — 1KB of a 32KB page. Since ALTER PAGE is read-modify-replace-whole-page, a +// truncated read would be written straight back over the real content, so the +// read must ask for the whole tree. Real pages nest far shallower than this +// bound, and hasTruncationSentinel catches it if one ever does not. +const pgReadFullDepth = 1000 + // pgReadPage reads a page's current high-level content tree via pg_read_page. // The result is the same LightPage shape pg_patch_page accepts, so it round-trips // for read-modify-write (ALTER PAGE). func (b *Backend) pgReadPage(moduleName, pageName string) (map[string]any, error) { - res, err := b.client.CallTool("pg_read_page", map[string]any{ + args := map[string]any{ "moduleName": moduleName, "pageName": pageName, - }) + } + // Only 11.13+ accepts `depth`; older servers declare the tool + // additionalProperties:false and would reject the whole call. + if b.client.SupportsToolArg("pg_read_page", "depth") { + args["depth"] = pgReadFullDepth + } + res, err := b.client.CallTool("pg_read_page", args) if err != nil { return nil, err } @@ -191,9 +213,48 @@ func (b *Backend) pgReadPage(moduleName, pageName string) (map[string]any, error if err := json.Unmarshal([]byte(text), &content); err != nil { return nil, fmt.Errorf("pg_read_page %s.%s: parsing content: %w", moduleName, pageName, err) } + // Guard, don't drop (ADR-0005). A truncated read is unusable for + // read-modify-write: writing it back replaces real widgets with the + // sentinel. Refuse the read rather than let a partial page reach a write. + if hasTruncationSentinel(content) { + return nil, fmt.Errorf( + "pg_read_page %s.%s: Studio Pro returned a depth-truncated page (%q placeholders); "+ + "mxcli cannot safely modify a page it cannot read whole. Modify this page in "+ + "Studio Pro, and please report the page name — mxcli requests the full depth, "+ + "so a truncated read means this Studio Pro version truncates in a way mxcli "+ + "does not yet handle", + moduleName, pageName, pgTruncationSentinel) + } return content, nil } +// hasTruncationSentinel reports whether a LightPage carries a depth-truncation +// placeholder, i.e. the string "..." standing where an element should be. +// +// It only matches the sentinel as an *array element*, which is where a dropped +// widget lands. A "..." that is a property value (a caption or a title reading +// "...") is legitimate page content and must not trip the guard. +func hasTruncationSentinel(v any) bool { + switch t := v.(type) { + case map[string]any: + for _, e := range t { + if hasTruncationSentinel(e) { + return true + } + } + case []any: + for _, e := range t { + if s, ok := e.(string); ok && s == pgTruncationSentinel { + return true + } + if hasTruncationSentinel(e) { + return true + } + } + } + return false +} + // pgWritePage writes a whole page (create or full overwrite) via pg_patch_page. // // Studio Pro 11.12 removed pg_write_page in favour of pg_patch_page (RFC 6902 diff --git a/mdl/backend/mcp/page_depth_test.go b/mdl/backend/mcp/page_depth_test.go new file mode 100644 index 000000000..ae5ca7c42 --- /dev/null +++ b/mdl/backend/mcp/page_depth_test.go @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mcp + +import ( + "encoding/json" + "strings" + "testing" +) + +// Studio Pro 11.13 gave pg_read_page a `depth` argument defaulting to 4, and +// replaces anything deeper with the string "...". mxcli's ALTER PAGE is +// read-modify-replace-whole-page, so a truncated read is written back over the +// live page. Captured against 11.13: an ordinary page read at the default depth +// collapsed from 32,594 bytes to 1,052, its whole widget tree reduced to +// {"widgets":["...","..."]}; writing that back was rejected with +// PROP_NOT_PRIMITIVE, breaking ALTER PAGE outright. +// +// These tests pin both halves of the fix: request the full depth where the +// server understands it, and refuse a truncated read rather than write it back. + +// deepPage is a LightPage nested past the 11.13 default depth of 4. +const deepPage = `{ + "title": "Deep", + "layout": "Atlas_Core.Atlas_Default", + "widgets": [ + {"$Type": "Pages$Content", "slot": "Main", "widgets": [ + {"$Type": "Pages$DivContainer", "name": "lvl1", "widgets": [ + {"$Type": "Pages$DivContainer", "name": "lvl2", "widgets": [ + {"$Type": "Pages$ActionButton", "name": "btnDeep", "ct:caption": "Deep"} + ]} + ]} + ]} + ] +}` + +func TestPgReadPage_RequestsFullDepth_WhenServerSupportsIt(t *testing.T) { + f := newFakePED(t, func(name string, _ map[string]any) (string, bool) { + return deepPage, false + }) + f.tools = map[string][]string{ + // The 11.13 shape. + "pg_read_page": {"moduleName", "pageName", "depth", "paths"}, + } + b := &Backend{client: f.connectClient(t)} + + if _, err := b.pgReadPage("PgTest", "Deep"); err != nil { + t.Fatalf("pgReadPage: %v", err) + } + call, ok := f.callByName("pg_read_page") + if !ok { + t.Fatal("pg_read_page was never called") + } + got, ok := call.Args["depth"] + if !ok { + t.Fatal("depth was not sent; an 11.13 server would truncate the page to depth 4") + } + if n, _ := got.(float64); int(n) != pgReadFullDepth { + t.Fatalf("depth = %v, want %d", got, pgReadFullDepth) + } +} + +func TestPgReadPage_OmitsDepth_OnOlderServers(t *testing.T) { + // 11.11/11.12 declare pg_read_page additionalProperties:false without a + // `depth` property, so sending one would fail the whole call. + f := newFakePED(t, func(name string, args map[string]any) (string, bool) { + if _, sent := args["depth"]; sent { + return "unknown argument 'depth'", true + } + return deepPage, false + }) + f.tools = map[string][]string{"pg_read_page": {"moduleName", "pageName"}} + b := &Backend{client: f.connectClient(t)} + + if _, err := b.pgReadPage("PgTest", "Deep"); err != nil { + t.Fatalf("pgReadPage against a pre-11.13 server: %v", err) + } + call, _ := f.callByName("pg_read_page") + if _, sent := call.Args["depth"]; sent { + t.Fatal("depth must not be sent to a server that does not advertise it") + } +} + +func TestPgReadPage_RefusesTruncatedPage(t *testing.T) { + // What 11.13 actually returned for Administration.Account_Overview at the + // default depth — the entire widget tree replaced by sentinels. + const truncated = `{"title":"Accounts","layout":"Atlas_Core.Atlas_Default", + "parameters":[],"variables":[], + "widgets":[{"$Type":"Pages$Content","slot":"Main","widgets":["...","..."]}]}` + f := newFakePED(t, func(name string, _ map[string]any) (string, bool) { + return truncated, false + }) + f.tools = map[string][]string{"pg_read_page": {"moduleName", "pageName", "depth"}} + b := &Backend{client: f.connectClient(t)} + + _, err := b.pgReadPage("Administration", "Account_Overview") + if err == nil { + t.Fatal("expected a truncated page to be refused; returning it lets ALTER PAGE write placeholders over real widgets") + } + if !strings.Contains(err.Error(), "truncated") { + t.Fatalf("error should name the cause, got: %v", err) + } +} + +func TestHasTruncationSentinel(t *testing.T) { + tests := []struct { + name string + doc string + want bool + }{ + {"clean page", deepPage, false}, + {"sentinel in widgets array", `{"widgets":["..."]}`, true}, + {"sentinel nested deep", `{"a":{"b":[{"c":["..."]}]}}`, true}, + // A page may legitimately contain "..." as text; only a sentinel + // standing where an element belongs (an array item) counts. + {"ellipsis caption is not truncation", `{"widgets":[{"ct:caption":"..."}]}`, false}, + {"ellipsis title is not truncation", `{"title":"..."}`, false}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var v any + if err := json.Unmarshal([]byte(tc.doc), &v); err != nil { + t.Fatalf("bad fixture: %v", err) + } + if got := hasTruncationSentinel(v); got != tc.want { + t.Fatalf("hasTruncationSentinel = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/mdl/backend/mcp/testdata/tools-11.13.json b/mdl/backend/mcp/testdata/tools-11.13.json new file mode 100644 index 000000000..d17a77252 --- /dev/null +++ b/mdl/backend/mcp/testdata/tools-11.13.json @@ -0,0 +1,1329 @@ +{ + "tools": [ + { + "description": "List files matching a glob pattern. The pattern must start with one of the registered root paths.\n\n\n /jsactions\n Access JavaScript action source files in the app. Paths are of the form \"<module_name_lowercase>/actions/<action_name>.js\"\n (for example, \"myfirstmodule/actions/myaction.js\").\n\n\n /theme\n Access all Atlas UI theme files in the app. Two path trees are available under this root:\n - /theme/web/ \u2014 app-specific overrides: custom-variables.scss, main.scss, settings.json\n - /theme/themesource/<module>/web/ \u2014 module SCSS/CSS sources and design-properties.json\n - /theme/themesource/<module>/settings.json \u2014 module-level settings\n Examples: /theme/web/custom-variables.scss, /theme/themesource/atlas_core/web/main.scss\n\n\n /themesource\n Atlas UI module sources. Contains SCSS/CSS files, design-properties.json, and settings.json for each module. Paths are relative to themesource/ (e.g. 'atlas_core/web/main.scss', 'my_module/web/design-properties.json').\n\n\n /pagegen/appearanceVFSPlugin\n Design property definitions for each element type, reflecting the merged and effective set across all active theme modules \u2014 matching what a user sees in Studio Pro. Load the 'appearance' skill for guidance on how to use these files.\n\n\n /pagegen/customWidgetsVFS\n Provide information about available custom widgets to the pagegen skill along with the relevant schemas.\n The file contents follow the same format as defined in the default schema tools and ped tools.\n\n", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "pattern": { + "description": "Glob pattern that MUST start with a registered root path (e.g., '/themes/*.scss', '/themes/**/*.css'). Supports glob patterns of arbitrary complexity.", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + }, + "name": "glob" + }, + { + "description": "Installs a Mendix Marketplace module into the currently open Studio Pro project. The user must be signed in to Mendix Platform. Use Component_GetComponentIDsByCriteria to look up versionId before calling this tool.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "conflictResolution": { + "description": "How to handle conflicts when a module with the same name already exists. 'auto' (default): replace if exists, add otherwise. 'add': always add as a new module. 'replace': replace the existing module (fails if not found).", + "enum": [ + "auto", + "add", + "replace" + ], + "type": "string" + }, + "moduleName": { + "description": "Display name of the module (e.g. 'OpenAI Connector'). Used to match an existing module for replacement.", + "type": "string" + }, + "versionId": { + "description": "Marketplace version UUID of the specific module version to install.", + "type": "string" + } + }, + "required": [ + "versionId" + ], + "type": "object" + }, + "name": "install_marketplace_module" + }, + { + "description": "Lists all modules in the app, including whether each module is writable and whether it comes from the Marketplace. Writable modules are user-created; non-writable modules are system modules or Marketplace modules.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "name": "list_modules" + }, + { + "description": "Discover component data with unparalleled ease! This AI-powered tool intelligently navigates the marketplace, helping you locate specific components whether you know their exact name or just a general description. Beyond just finding, it meticulously gathers and presents all associated metadata, ensuring you have a complete and detailed informational profile to support your projects. It's designed to bring comprehensive component insights directly to you, faster than ever.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": {}, + "properties": { + "SearchCriteria": { + "type": "string" + } + }, + "required": [ + "SearchCriteria" + ], + "type": "object" + }, + "name": "mcp_mendix-marketplace_Component_GetComponentIDsByCriteria" + }, + { + "description": "Check multiple documents for errors in a single call (mandatory after final create/update).\n\n- documents: array of {documentType, documentName} objects\n- Returns: \"No errors found.\" when all documents are clean; otherwise only the documents with errors are listed\n- If errors in any document: attempt ONE fix via ped_update_document \u2192 re-check\n- If errors persist after one fix attempt: report error and suggested solution to user, then STOP\n- If no errors in all documents: verification is done.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "documents": { + "description": "Array of documents to check for errors. Can check multiple documents in a single call.", + "items": { + "additionalProperties": false, + "properties": { + "documentName": { + "description": "Fully qualified document name.", + "type": "string" + }, + "documentType": { + "description": "The full type name of the document (e.g., 'Microflows$Microflow', 'Pages$Page', 'DomainModels$DomainModel', 'Workflows$Workflow')", + "type": "string" + } + }, + "required": [ + "documentType", + "documentName" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "documents" + ], + "type": "object" + }, + "name": "ped_check_errors" + }, + { + "description": "Create one or more documents in a single call. Never create domain models.\n\n- documents: array of {documentType, moduleName, documentName, documentContent, folderPath?} objects\n- folderPath: optional folder within the module (e.g. \"FolderA/SubFolderB\"); folders are created if missing; omit for module root\n- Must call ped_find_document first for each; if a match exists, do NOT create\n- Get schemas before creating; read critical:true property descriptions\n- Include $Type for all $constructor/$element; NEVER include $ID\n- Use $id(/path) for by-id references: \"owner\": \"$id(/entities/0)\"\n- Prefer batching independent documents of the same type (e.g. multiple enumerations) to reduce round-trips", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "documents": { + "description": "Array of documents to create.", + "items": { + "additionalProperties": false, + "properties": { + "documentContent": { + "description": "The content of the document. Must conform to the document type schema obtained from 'get_document_schema'." + }, + "documentName": { + "description": "The name of the new document to create. Must start with a letter or underscore and can only contain letters, digits and underscores.", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$", + "type": "string" + }, + "documentType": { + "description": "The full type name of the document to create (e.g., 'Microflows$Microflow', 'Pages$Page', 'Workflows$Workflow')", + "type": "string" + }, + "folderPath": { + "description": "Folder path within the module where the document will be created (e.g. \"FolderA/SubFolderB\"). Folders are created automatically if they do not exist. Omit to create at the module root.", + "type": "string" + }, + "moduleName": { + "description": "The name of the module where to create the document", + "type": "string" + } + }, + "required": [ + "documentType", + "moduleName", + "documentName", + "documentContent" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "documents" + ], + "type": "object" + }, + "name": "ped_create_document" + }, + { + "description": "Creates a new module in the Mendix application.\nDo NOT create a module if one with the same name already exists.\nCreate a module only if the user explicitly requests it. If the user did not mention it, but you think it is a good idea, ask for confirmation first before creating.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "moduleName": { + "description": "The name of the new module to create.", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$", + "type": "string" + } + }, + "required": [ + "moduleName" + ], + "type": "object" + }, + "name": "ped_create_module" + }, + { + "description": "Find documents by module name and type (mandatory before creating to avoid duplicates).\n\n- Returns: { foundDocuments: [{ qualifiedName: \"Module.DocName\", folderPath: \"FolderA/SubFolderB\" }] }\n- folderPath is \"\" for documents directly in the module root\n- DO NOT USE 'DomainModels$DomainModel' \u2014 it always exists as a nameless document in a module\n- If returns matching results: MUST read each to verify \u2192 update if confirmed", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "documentType": { + "description": "The full type name of the document to find (e.g., 'Microflows$Microflow', 'Pages$Page', 'Workflows$Workflow'). DO NOT USE 'DomainModels$DomainModel' AS IT ALWAYS EXISTS AS A NAMELESS DOCUMENT IN A MODULE.", + "type": "string" + }, + "moduleName": { + "description": "The module where to look for the document", + "type": "string" + } + }, + "required": [ + "moduleName", + "documentType" + ], + "type": "object" + }, + "name": "ped_find_document" + }, + { + "description": "Get TypeScript-like schemas for one or more element types (mandatory before creating/adding/updating).\n\n- elementTypes: array of exact $Type values (e.g., [\"Microflows$Microflow\", \"DomainModels$Entity\"]).\n- kind:\n - \"constructor\" (default): the shape to CREATE a document or ADD a new element. Use before any create/add.\n - \"element\": the full shape you see when READING a document, and to UPDATE properties not exposed by the constructor.\n- IMPORTANT: If you request 'constructor' schema but the output provides 'element type' declarations, this means\n that this particular type does not have constructor schema defined and its element shape is used for creation.\n- Nested concrete elements are referenced as Element<'...'> and printed as their own declaration in the same result.\n- Abstract elements are shown as ChooseAbstractType<{...}>: pick ONE concrete type and request its schema.\n- Request every schema you need in a single call to reduce round-trips.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "elementTypes": { + "description": "Array of full type names to get schemas for (e.g., ['Microflows$ShowMessageAction', 'DomainModels$Entity']). Fetch multiple schemas in one call to reduce round-trips.", + "items": { + "type": "string" + }, + "type": "array" + }, + "kind": { + "default": "constructor", + "description": "Which shape to return: 'constructor' for creating/adding elements, or 'element' for the full read/update shape.", + "enum": [ + "element", + "constructor" + ], + "type": "string" + } + }, + "required": [ + "elementTypes" + ], + "type": "object" + }, + "name": "ped_get_schema" + }, + { + "description": "List the immediate contents of a module or a specific folder within it.\n\n- folderPath: optional path within the module (e.g. \"FolderA/SubFolderB\"); omit for module root\n- Returns: { module, folderPath, documents: [{name, type}], folders: [{name, path}] }\n- Only immediate children are returned \u2014 call again with a folder's path to explore deeper\n- Use to understand module structure, locate documents, or verify a folder exists before creating", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "folderPath": { + "description": "Path within the module to list (e.g. \"FolderA/SubFolderB\"). Omit to list the module root.", + "type": "string" + }, + "moduleName": { + "description": "The name of the module to inspect", + "type": "string" + } + }, + "required": [ + "moduleName" + ], + "type": "object" + }, + "name": "ped_list_folder" + }, + { + "description": "Read a Mendix document or nested elements at specified paths.\n\n- documentName: domain model = module only (\"MyFirstModule\"); project-level documents (e.g. \"Navigation$NavigationDocument\") = omit entirely; others = fully-qualified (\"MyFirstModule.MyMicroflow\")\n- paths: JSON Pointer array ([\"/\"], [\"/entities/0\", \"/associations\"]) - defaults to [\"/\"] if omitted\n- Returns: { folderPath: \"FolderA/SubFolderB\", results: [...] } \u2014 folderPath is \"\" for module-root documents\n- Each result: value at path expanded one level; children are stubs. Re-read child paths to expand further.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "documentName": { + "description": "Fully qualified document name. Omit for project-level documents (e.g. Navigation$NavigationDocument)", + "type": "string" + }, + "documentType": { + "description": "The full type name of the document (e.g., 'Microflows$Microflow', 'Pages$Page', 'DomainModels$DomainModel', 'Workflows$Workflow', 'Navigation$NavigationDocument')", + "type": "string" + }, + "paths": { + "description": "Array of JSON pointer paths to read. Use [\"/\"] or omit for root. Example: [\"/entities/0\", \"/entities/1\", \"/associations\"]", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "documentType" + ], + "type": "object" + }, + "name": "ped_read_document" + }, + { + "description": "Update an existing document with multiple set/add/remove operations in a single atomic call.\n\nREMINDER: After running ped_check_error, YOU GET TO UPDATE THE DOCUMENT EXACTLY ONCE. After that, if there are still errors, you need to report them and STOP.\n\nIMPORTANT: Set operations can only be used for non-element properties or null/undefined element properties, and can NEVER be used on arrays.\nConversely, add operations can ONLY be used on arrays to add new elements.\n\n- documentName: domain model = module only; project-level documents (e.g. Navigation$NavigationDocument) = omit entirely; others = fully-qualified\n- operations: array of {path, operation} objects. Each operation has:\n - path: JSON pointer path to the property\n - operation: set (primitives only, unless currently null or undefined), add (to arrays), or remove (from arrays by index)\n- Before: read document and get schemas\n- CRITICAL FOR SUCESS WHEN CREATING ELEMENTS: Include $Type when adding; NEVER include $ID.\n If you are adding new elements, you MUST first get their schema. Never assume you know the structure of the element from previous read operations.\n- Use $id(/path) for by-id references\n- Remove: path must point to an array property; specify the index of the element to remove\n- Operations are validated and applied atomically; stops on first error\n- INDEX SHIFTING IS HANDLED FOR YOU \u2014 DO NOT COMPENSATE FOR IT. Every `index` you provide refers to the array as it exists BEFORE this call (the exact state you last read). Internally, all add/remove operations on the SAME path are reordered and applied in DESCENDING index order, so no operation ever shifts the position targeted by another. There is no \"+1/-1\" bookkeeping to do; never adjust an index to account for another operation in the same call.\n - Removing multiple items: pass the indices exactly as you read them, in any order. To delete the items currently at index 1 and 3, use remove(1) and remove(3) \u2014 do NOT use remove(1) then remove(2).\n - Adding at distinct positions: use the CURRENT index of the element the new one should sit before. On [A,B,C], add(X,1) + add(Y,2) yields [A,X,B,Y,C].\n - Adding several NEW elements at the SAME index: give them all that same index and list them in the order you want them to appear. To turn [Start,End] into [Start,A,B,End], call add(1,A) and add(1,B) in that order.\n- CROSS-PATH ORDER STILL MATTERS: operations on DIFFERENT paths are applied in the order you list them, and are NOT reordered. When a new element references another element added in the same call via `$id(/path)`, add the referenced element (on its own path) before the element that references it.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "documentName": { + "description": "Domain model = module name only. Project-level documents (e.g. Navigation$NavigationDocument) = omit entirely. Others = fully-qualified name.", + "type": "string" + }, + "documentType": { + "description": "The full type name of the document to update (e.g., 'Microflows$Microflow', 'Pages$Page', 'Navigation$NavigationDocument')", + "type": "string" + }, + "operations": { + "description": "Array of update operations to apply. Each operation has a path and an operation (set, add, or remove).", + "items": { + "additionalProperties": false, + "properties": { + "operation": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "type": { + "const": "set", + "description": "Set a property to a new value. You can never set array properties, use 'add' operation for that. You can always set primitive values or references. You can set element-valued properties only if they are currently unset (equal to `null` or `undefined`). If element-valued properties have a non-nullable value they CANNOT be set again. When setting a null/undefined property to a non-primitive value, the value MUST include $Type and conform to the $constructor schema if one is available, otherwise the full $element schema.", + "type": "string" + }, + "value": {} + }, + "required": [ + "type", + "value" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "index": { + "description": "The index at which to add the element. If omitted, the element is added to the end of the array. Add the index ONLY if the schema of the document explicitly mandates that elements need to be at certain positions (e.g., a StartWorkflowActivity must be first in a workflow's flow). In all other cases, you can leave it unspecified. REMEMBER: Indices are zero-based!", + "type": "number" + }, + "type": { + "const": "add", + "description": "Add an element to an array property. Path must point to the array and NOT include an index (e.g., '/entities' or '/flows')", + "type": "string" + }, + "value": { + "description": "The element to add (must be a complete element object)" + } + }, + "required": [ + "type", + "value" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "index": { + "description": "The path of this operation must point to an array. This argument specifies the index of the element to remove in the array.", + "type": "number" + }, + "type": { + "const": "remove", + "description": "Remove an element from an array property", + "type": "string" + } + }, + "required": [ + "type", + "index" + ], + "type": "object" + } + ] + }, + "path": { + "description": "JSON pointer path to the property to update", + "type": "string" + } + }, + "required": [ + "path", + "operation" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "documentType", + "operations" + ], + "type": "object" + }, + "name": "ped_update_document" + }, + { + "description": "Create a new page or patch an existing one using JSON Patch operations (RFC 6902).\n**Stick to the JSON Schema:** Strictly adhere to the pg_patch_page JSON schema when creating your input.\n\n**Creating a page:** supply a single { \"op\": \"replace\", \"path\": \"\", \"value\": { } } operation. The page will be created if it does not exist yet.\n\n**Patching an existing page:** supply one or more targeted operations against the LightPage structure returned by pg_read_page. Prefer this over a root replace when only a small section needs to change. IMPORTANT The path MUST always refer to and existing element.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "moduleName": { + "description": "Module name of the page. Example: 'MyFirstModule'. Note that this is not the fully qualified name, just the module part.", + "type": "string" + }, + "pageName": { + "description": "Name of the page to patch. Example: 'MyFirstPage'.", + "type": "string" + }, + "patches": { + "description": "Ordered list of JSON Patch operations (RFC 6902) to apply to the current page. Operations are applied sequentially. Paths are JSON Pointers into the LightPage structure returned by pg_read_page.\n\n**CRITICAL: This must be an ARRAY, not a JSON string. Do NOT stringify the patches array.**\n\n**CRITICAL: To add an item to an array, always use op:'add' with a path ending in '/-' \u2014 never use op:'replace' on the array itself.**\n\nExample \u2014 appending a tab to a TabContainer's tabPages:\n CORRECT: { \"op\": \"add\", \"path\": \"/widgets/0/widgets/0/tabPages/-\", \"value\": { \"$Type\": \"Pages$TabPage\", ... } }\n INCORRECT: { \"op\": \"replace\", \"path\": \"/widgets/0/widgets/0/tabPages\", \"value\": [ /* full array */ ] }\n\nReplacing the entire array is rejected by schema validation. Use targeted add/remove/replace operations on individual items instead.\n\n**CRITICAL: When removing multiple items from the same array, always list the remove operations in descending index order (highest index first).** Removing an item shifts all subsequent indices down by one, so removing index 1 before index 2 means what was index 2 is now index 1 \u2014 your next operation will hit the wrong item. Highest-first avoids this entirely.\n\nExample \u2014 removing items at index 1 and 3 from the same array:\n CORRECT: remove /widgets/0/widgets/3, then remove /widgets/0/widgets/1\n INCORRECT: remove /widgets/0/widgets/1, then remove /widgets/0/widgets/3 (index 3 is now stale)", + "items": { + "additionalProperties": false, + "properties": { + "op": { + "description": "The patch operation to perform", + "enum": [ + "add", + "remove", + "replace" + ], + "type": "string" + }, + "path": { + "description": "JSON Pointer path (RFC 6901) into the LightPage object. E.g., '/widgets/0/widgets/1' targets the second widget in the first content slot. Use '-' as the final segment to append to an array. IMPORTANT The path MAY ONLY refer to an existing element or be empty to target the page root.", + "type": "string" + }, + "value": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "layout": { + "description": "The layout used by the page, if any", + "type": "string" + }, + "parameters": { + "description": "Array of Pages$PageParameter objects defining input parameters for the page", + "items": { + "additionalProperties": {}, + "properties": { + "$Type": { + "const": "Pages$PageParameter", + "type": "string" + } + }, + "required": [ + "$Type" + ], + "type": "object" + }, + "type": "array" + }, + "title": { + "description": "Title of the page", + "type": "string" + }, + "variables": { + "description": "Array of Pages$LocalVariable objects defining local variables for storing temporary page state", + "items": { + "additionalProperties": {}, + "properties": { + "$Type": { + "const": "Pages$LocalVariable", + "type": "string" + }, + "defaultValue": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Default value for the variable (null if none)" + }, + "name": { + "description": "Name of the local variable", + "type": "string" + }, + "variableType": { + "additionalProperties": {}, + "description": "The type of data this variable holds", + "properties": { + "$Type": { + "enum": [ + "DataTypes$BooleanType", + "DataTypes$StringType", + "DataTypes$IntegerType", + "DataTypes$DecimalType", + "DataTypes$FloatType", + "DataTypes$DateTimeType", + "DataTypes$EnumerationType" + ], + "type": "string" + } + }, + "required": [ + "$Type" + ], + "type": "object" + } + }, + "required": [ + "$Type", + "name", + "variableType", + "defaultValue" + ], + "type": "object" + }, + "type": "array" + }, + "widgets": { + "description": "Array of Pages$Content objects", + "items": { + "additionalProperties": {}, + "properties": { + "$Type": { + "enum": [ + "Pages$Content", + "Pages$LayoutGrid", + "Pages$LayoutGridRow", + "Pages$LayoutGridColumn", + "Pages$DataView", + "Pages$ActionButton", + "Pages$TextBox", + "Pages$TextArea", + "Pages$DatePicker", + "Pages$CheckBox", + "Pages$RadioButtonGroup", + "Pages$DynamicText", + "Pages$DivContainer", + "Pages$TabContainer", + "Pages$TabPage", + "Pages$ListView", + "CustomWidgets$CustomWidget", + "CustomWidgets$WidgetObject" + ], + "type": "string" + }, + "slot": { + "description": "The slot name where the content widget is placed, e.g., 'Main'", + "type": "string" + }, + "widgets": { + "description": "List of widgets on the page. Each widget MUST include $Type property and all required properties from documentation", + "items": { + "additionalProperties": {}, + "properties": { + "$Type": { + "enum": [ + "Pages$Content", + "Pages$LayoutGrid", + "Pages$LayoutGridRow", + "Pages$LayoutGridColumn", + "Pages$DataView", + "Pages$ActionButton", + "Pages$TextBox", + "Pages$TextArea", + "Pages$DatePicker", + "Pages$CheckBox", + "Pages$RadioButtonGroup", + "Pages$DynamicText", + "Pages$DivContainer", + "Pages$TabContainer", + "Pages$TabPage", + "Pages$ListView", + "CustomWidgets$CustomWidget", + "CustomWidgets$WidgetObject" + ], + "type": "string" + }, + "appearance": { + "additionalProperties": {}, + "description": "Appearance settings for the widget, including CSS classes and styles", + "properties": { + "class": { + "description": "CSS classes to apply to the widget", + "type": "string" + }, + "style": { + "description": "Inline CSS styles to apply to the widget", + "type": "string" + } + }, + "required": [ + "class", + "style" + ], + "type": "object" + } + }, + "required": [ + "$Type", + "appearance" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "$Type", + "slot", + "widgets" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "title", + "layout", + "parameters", + "widgets" + ], + "type": "object" + }, + { + "additionalProperties": {}, + "properties": { + "$Type": { + "enum": [ + "Pages$Content", + "Pages$LayoutGrid", + "Pages$LayoutGridRow", + "Pages$LayoutGridColumn", + "Pages$DataView", + "Pages$ActionButton", + "Pages$TextBox", + "Pages$TextArea", + "Pages$DatePicker", + "Pages$CheckBox", + "Pages$RadioButtonGroup", + "Pages$DynamicText", + "Pages$DivContainer", + "Pages$TabContainer", + "Pages$TabPage", + "Pages$ListView", + "CustomWidgets$CustomWidget", + "CustomWidgets$WidgetObject" + ], + "type": "string" + }, + "slot": { + "description": "The slot name where the content widget is placed, e.g., 'Main'", + "type": "string" + }, + "widgets": { + "description": "List of widgets on the page. Each widget MUST include $Type property and all required properties from documentation", + "items": { + "additionalProperties": {}, + "properties": { + "$Type": { + "enum": [ + "Pages$Content", + "Pages$LayoutGrid", + "Pages$LayoutGridRow", + "Pages$LayoutGridColumn", + "Pages$DataView", + "Pages$ActionButton", + "Pages$TextBox", + "Pages$TextArea", + "Pages$DatePicker", + "Pages$CheckBox", + "Pages$RadioButtonGroup", + "Pages$DynamicText", + "Pages$DivContainer", + "Pages$TabContainer", + "Pages$TabPage", + "Pages$ListView", + "CustomWidgets$CustomWidget", + "CustomWidgets$WidgetObject" + ], + "type": "string" + }, + "appearance": { + "additionalProperties": {}, + "description": "Appearance settings for the widget, including CSS classes and styles", + "properties": { + "class": { + "description": "CSS classes to apply to the widget", + "type": "string" + }, + "style": { + "description": "Inline CSS styles to apply to the widget", + "type": "string" + } + }, + "required": [ + "class", + "style" + ], + "type": "object" + } + }, + "required": [ + "$Type", + "appearance" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "$Type", + "slot", + "widgets" + ], + "type": "object" + }, + { + "additionalProperties": {}, + "properties": { + "$Type": { + "enum": [ + "Pages$Content", + "Pages$LayoutGrid", + "Pages$LayoutGridRow", + "Pages$LayoutGridColumn", + "Pages$DataView", + "Pages$ActionButton", + "Pages$TextBox", + "Pages$TextArea", + "Pages$DatePicker", + "Pages$CheckBox", + "Pages$RadioButtonGroup", + "Pages$DynamicText", + "Pages$DivContainer", + "Pages$TabContainer", + "Pages$TabPage", + "Pages$ListView", + "CustomWidgets$CustomWidget", + "CustomWidgets$WidgetObject" + ], + "type": "string" + }, + "appearance": { + "additionalProperties": {}, + "description": "Appearance settings for the widget, including CSS classes and styles", + "properties": { + "class": { + "description": "CSS classes to apply to the widget", + "type": "string" + }, + "style": { + "description": "Inline CSS styles to apply to the widget", + "type": "string" + } + }, + "required": [ + "class", + "style" + ], + "type": "object" + } + }, + "required": [ + "$Type", + "appearance" + ], + "type": "object" + }, + { + "additionalProperties": {}, + "properties": { + "class": { + "description": "CSS classes to apply to the widget", + "type": "string" + }, + "style": { + "description": "Inline CSS styles to apply to the widget", + "type": "string" + } + }, + "required": [ + "class", + "style" + ], + "type": "object" + }, + { + "additionalProperties": {}, + "properties": { + "$Type": { + "const": "Pages$PageParameter", + "type": "string" + } + }, + "required": [ + "$Type" + ], + "type": "object" + }, + { + "additionalProperties": {}, + "properties": { + "$Type": { + "const": "Pages$LocalVariable", + "type": "string" + }, + "defaultValue": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Default value for the variable (null if none)" + }, + "name": { + "description": "Name of the local variable", + "type": "string" + }, + "variableType": { + "additionalProperties": {}, + "description": "The type of data this variable holds", + "properties": { + "$Type": { + "enum": [ + "DataTypes$BooleanType", + "DataTypes$StringType", + "DataTypes$IntegerType", + "DataTypes$DecimalType", + "DataTypes$FloatType", + "DataTypes$DateTimeType", + "DataTypes$EnumerationType" + ], + "type": "string" + } + }, + "required": [ + "$Type" + ], + "type": "object" + } + }, + "required": [ + "$Type", + "name", + "variableType", + "defaultValue" + ], + "type": "object" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "items": { + "additionalProperties": {}, + "properties": { + "$Type": { + "enum": [ + "Pages$Content", + "Pages$LayoutGrid", + "Pages$LayoutGridRow", + "Pages$LayoutGridColumn", + "Pages$DataView", + "Pages$ActionButton", + "Pages$TextBox", + "Pages$TextArea", + "Pages$DatePicker", + "Pages$CheckBox", + "Pages$RadioButtonGroup", + "Pages$DynamicText", + "Pages$DivContainer", + "Pages$TabContainer", + "Pages$TabPage", + "Pages$ListView", + "CustomWidgets$CustomWidget", + "CustomWidgets$WidgetObject" + ], + "type": "string" + }, + "slot": { + "description": "The slot name where the content widget is placed, e.g., 'Main'", + "type": "string" + }, + "widgets": { + "description": "List of widgets on the page. Each widget MUST include $Type property and all required properties from documentation", + "items": { + "additionalProperties": {}, + "properties": { + "$Type": { + "enum": [ + "Pages$Content", + "Pages$LayoutGrid", + "Pages$LayoutGridRow", + "Pages$LayoutGridColumn", + "Pages$DataView", + "Pages$ActionButton", + "Pages$TextBox", + "Pages$TextArea", + "Pages$DatePicker", + "Pages$CheckBox", + "Pages$RadioButtonGroup", + "Pages$DynamicText", + "Pages$DivContainer", + "Pages$TabContainer", + "Pages$TabPage", + "Pages$ListView", + "CustomWidgets$CustomWidget", + "CustomWidgets$WidgetObject" + ], + "type": "string" + }, + "appearance": { + "additionalProperties": {}, + "description": "Appearance settings for the widget, including CSS classes and styles", + "properties": { + "class": { + "description": "CSS classes to apply to the widget", + "type": "string" + }, + "style": { + "description": "Inline CSS styles to apply to the widget", + "type": "string" + } + }, + "required": [ + "class", + "style" + ], + "type": "object" + } + }, + "required": [ + "$Type", + "appearance" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "$Type", + "slot", + "widgets" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + { + "items": { + "additionalProperties": {}, + "properties": { + "$Type": { + "enum": [ + "Pages$Content", + "Pages$LayoutGrid", + "Pages$LayoutGridRow", + "Pages$LayoutGridColumn", + "Pages$DataView", + "Pages$ActionButton", + "Pages$TextBox", + "Pages$TextArea", + "Pages$DatePicker", + "Pages$CheckBox", + "Pages$RadioButtonGroup", + "Pages$DynamicText", + "Pages$DivContainer", + "Pages$TabContainer", + "Pages$TabPage", + "Pages$ListView", + "CustomWidgets$CustomWidget", + "CustomWidgets$WidgetObject" + ], + "type": "string" + }, + "appearance": { + "additionalProperties": {}, + "description": "Appearance settings for the widget, including CSS classes and styles", + "properties": { + "class": { + "description": "CSS classes to apply to the widget", + "type": "string" + }, + "style": { + "description": "Inline CSS styles to apply to the widget", + "type": "string" + } + }, + "required": [ + "class", + "style" + ], + "type": "object" + } + }, + "required": [ + "$Type", + "appearance" + ], + "type": "object" + }, + "type": "array" + }, + { + "items": { + "additionalProperties": {}, + "properties": { + "$Type": { + "const": "Pages$PageParameter", + "type": "string" + } + }, + "required": [ + "$Type" + ], + "type": "object" + }, + "type": "array" + }, + { + "items": { + "additionalProperties": {}, + "properties": { + "$Type": { + "const": "Pages$LocalVariable", + "type": "string" + }, + "defaultValue": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Default value for the variable (null if none)" + }, + "name": { + "description": "Name of the local variable", + "type": "string" + }, + "variableType": { + "additionalProperties": {}, + "description": "The type of data this variable holds", + "properties": { + "$Type": { + "enum": [ + "DataTypes$BooleanType", + "DataTypes$StringType", + "DataTypes$IntegerType", + "DataTypes$DecimalType", + "DataTypes$FloatType", + "DataTypes$DateTimeType", + "DataTypes$EnumerationType" + ], + "type": "string" + } + }, + "required": [ + "$Type" + ], + "type": "object" + } + }, + "required": [ + "$Type", + "name", + "variableType", + "defaultValue" + ], + "type": "object" + }, + "type": "array" + }, + { + "additionalProperties": {}, + "description": "Free-form object, e.g. designProperties: { 'toggle:Cards style': true }", + "propertyNames": { + "type": "string" + }, + "type": "object" + } + ], + "description": "The value for 'add' or 'replace' operations. Omit for 'remove'." + } + }, + "required": [ + "op", + "path" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "moduleName", + "pageName", + "patches" + ], + "type": "object" + }, + "name": "pg_patch_page" + }, + { + "description": "Read a page or specific sub-sections of it. Supply an optional 'paths' list of JSON Pointers (RFC 6901) to return only those parts of the LightPage \u2014 e.g., ['/widgets/0', '/widgets/1'] returns the first two content slots. Results are returned as an array in the same order as the paths. Omit 'paths' to return the full page.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "depth": { + "description": "Maximum depth to return. Nodes deeper than this limit are replaced with '...'. Defaults to 4. Increase when you need to see deeper structure, or decrease for a high-level structural overview. IMPORTANT: when a node shows '...' and you need its contents, do NOT re-read from the root \u2014 instead use 'paths' pointing directly at that node with a higher depth.", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "type": "integer" + }, + "moduleName": { + "description": "Module name of the page. Example: 'MyFirstModule'. Note that this is not the fully qualified name, just the module part", + "type": "string" + }, + "pageName": { + "description": "Name of the page to read. Example: 'MyFirstPage'", + "type": "string" + }, + "paths": { + "description": "Optional list of JSON Pointers (RFC 6901) into the LightPage to return only specific sub-sections. E.g., ['/widgets/0', '/widgets/1/widgets/2'] returns the first content slot and the third widget inside the second slot. Omit to return the full page. When you need to drill into multiple areas, pass all paths in a single call rather than making multiple calls.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "moduleName", + "pageName" + ], + "type": "object" + }, + "name": "pg_read_page" + }, + { + "description": "Read the content of a file at the given virtual path. Use glob tool to discover available files and their paths.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "endLine": { + "description": "1-based end line (inclusive). Omit to read to the end.", + "type": "number" + }, + "path": { + "description": "Full virtual path of the file to read as returned by glob (e.g., '/themes/mytheme/main.scss')", + "type": "string" + }, + "startLine": { + "description": "1-based start line (inclusive). Omit to read from the beginning.", + "type": "number" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "read_file" + }, + { + "description": "Allows loading of skills or resources within skills that provide domain-specific instructions.\nAccepts an array of skills to load in a single call.\nSpecify name only when you want to load the skill content (for example, read_skill({skills: [{skillName: \"microflow-common\"}]})).\n\nSome skills may contain references to resources (for example markdown links, such as [Examples](references/examples.md)). To load such resources, issue a call to this tool with the name of the skill\nwhere the reference is located and the path to the resource (for example read_skill({skills: [{skillName: \"microflow-common\", resourcePath: \"references/examples.md\"}]})).\nThe COMPLETE list of resource a given skill references will be given as a at the end of the skill content. Use only these paths to access the resources.\n\nMultiple skills and resources can be loaded at once (for example read_skill({skills: [{skillName: \"microflow-common\"}, {skillName: \"microflow-xpath\"}]})).\nMake sure to bundle calling this tool with getting schemas as much as possible.\nCore skills ship with the product. Custom skills are added by the user or installed via marketplace modules. No custom skills are currently registered for this project.\n\n\n\nworkflow-update\nCritical rules for modifying workflows via ped_update_document. Use when adding, removing, or replacing activities, outcomes, or flows in existing workflows. Prevents breaking constraints around start activities, outcomes, and activity placement.\n\n\nworkflow-common\nEssential rules and patterns for Mendix workflows. Use when creating or modifying activities, decisions, user tasks, boundary events, event sub-processes, or workflow expressions. Includes constraints for outcomes, user targeting, and variable scope.\n\n\ndata-importer-common\nGuided conversational workflow for creating a Mendix Data Importer Template document and companion Import From File microflow from user-provided file structure. Load this skill when the user asks to create, set up, or configure a Data Importer template, or wants to import data from an Excel or CSV file.\n\n\nmicroflow-common\nKnowledge and tools needed for building and updating Mendix Microflow documents. Always use this skill whenever you are working with Microflows.\n\n\nmicroflow-expressions\nExpression language reference including operators, functions, and syntax for microflows\n\n\nmicroflow-xpath\nXPath constraint syntax and operators for filtering objects in microflows. Read this skill before performing any operation that involves XPath.\n\n\nmicroflow-unit-testing\nGenerate unit test microflows that validate existing microflow logic; use for new unit tests and follow-up test generation after microflow creation.\n\n\nvalidation-microflow\nSpecialized knowledge for creating Mendix Validation Microflows. Use this skill when creating or updating validation microflows, when the user asks to validate entity attributes, check required fields, create before-commit validation, or mentions VAL_* naming.\n\n\ndatabase-connector-common\nHow to create and configure a Mendix DatabaseConnection document for connecting to external databases (MySQL, MSSQL, Oracle, PostgreSQL, Snowflake, BYOD), and how to manage SQL queries and parameters on a DatabaseConnection. Load this skill whenever the user asks to create, set up, or configure a database connection, manage queries or parameters on a DatabaseConnection, or work with the Query External Database microflow activity. Also load this skill whenever Maia reads a microflow activity that queries an external database.\n\n\njavascript-action\nCommon knowledge needed for creating or updating JavaScript Action documents. Always use this skill whenever you are working with JavaScript Actions.\n\n\npage-gen-common\nCommon skills for generating Mendix pages using JSON.\n\n\nglyph-icons\nInstructions for working with glyph icons. Always load this file when the user requests help with glyph icon related tasks, like using a glyph icon on a page or in a menu.\n\n\ntheming\nKnowledge for working with Mendix Atlas UI theming \u2014 CSS variables, SCSS structure, and the module hierarchy. Load this skill whenever the user asks to modify styles, change theme colors, or work with the Atlas design system.\n\n\ndesign-properties\nKnowledge for working with Mendix design-properties.json files \u2014 property types, widget keys, module merging, exclusions, and JSON rules. Load this skill whenever the user asks to create, modify, or debug design properties for widgets.\n\n\nnavigation\nInstructions for working with Mendix navigation profiles and menus. ALWAYS load this file when making ANY change to the navigation document, even if the change seems trivial. This includes working with navigation profiles that are part of the navigation document. This is CRITICAL for maintaining the integrity of the navigation configuration and avoiding common pitfalls\n\n\nfolder-structure\nInstructions for placing Mendix documents in the correct module folder when creating or organizing them. Load this skill whenever the user asks to create documents, organize a module, or when you need to decide which folder a new document belongs in.\n\n\nview-entities\nInstructions for managing Mendix view entities and their OQL queries. Load this skill whenever the user asks to create, update, refine, inspect, or delete a view entity or ViewEntitySourceDocument, or whenever the user mentions OQL.\n\n\n", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "skills": { + "description": "Array of skills or skill resources to read.", + "items": { + "additionalProperties": false, + "properties": { + "resourcePath": { + "description": "Optional relative path to a resource within the skill (e.g., 'resources/examples.md'). If not provided, the main skill content will be returned. Provide only when you want to access a specific resource file within the skill.", + "type": "string" + }, + "skillName": { + "description": "The name of the skill to read (e.g., 'microflow-common').", + "type": "string" + } + }, + "required": [ + "skillName" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "skills" + ], + "type": "object" + }, + "name": "read_skill" + }, + { + "description": "Access the comprehensive Mendix knowledge base and official documentation to retrieve contextually relevant information.\nUse this tool when the user asks for documentation or conceptual information about Mendix features, best practices, technical details, troubleshooting, or any Mendix-related topic where factual and up-to-date information is required to formulate an accurate answer.\nThis helps ensure responses are grounded in the latest Mendix expertise.\nDo NOT use this tool to look up model element types, schemas, or structural information \u2014 it cannot answer those questions. Use the schema introspection tools for that purpose.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "query": { + "description": "The specific question, keyword, or Mendix-related topic from the user's request that needs to be searched in the knowledge base.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_mendix_knowledge_base" + }, + { + "description": "Create or update a file at the given path. You can write only to file domains mentioned in the glob tool description.", + "execution": { + "taskSupport": "forbidden" + }, + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "newContent": { + "description": "The content to write", + "type": "string" + }, + "path": { + "description": "Full path of the file to create or update. Must start with a valid file domain root path (e.g., '/themes/mytheme/main.scss')", + "type": "string" + }, + "span": { + "additionalProperties": false, + "description": "Optional line range to replace. Omit to replace the entire file content.", + "properties": { + "endLine": { + "description": "1-based end line (inclusive) of the range to replace. Omit to write until the end.", + "type": "number" + }, + "startLine": { + "description": "1-based start line (inclusive) of the range to replace. Omit to write from start.", + "type": "number" + } + }, + "type": "object" + } + }, + "required": [ + "path", + "newContent" + ], + "type": "object" + }, + "name": "write_file" + } + ] +} \ No newline at end of file