Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
41d4d1e
fix(catalog): reference the mappings a flow applies and a mapping's J…
ako Nov 8, 2026
f338704
Merge origin/main into fix/1861-mapping-refs
ako Nov 8, 2026
c125e0c
fix(agents): a rewrite keeps what Agent Editor stored in the document
ako Nov 8, 2026
da2e19d
feat(agents): tool approval, display text and streaming in MDL (#2021…
ako Nov 8, 2026
731b6ad
Merge remote-tracking branch 'origin/main' into fix/agent-document-carry
ako Nov 8, 2026
ffaea0d
docs(skills): publish an app as an MCP server with runtime configurat…
ako Nov 8, 2026
be463de
docs(skills): mcp-server description under 600 chars, list callers
ako Nov 8, 2026
83e8ec3
Merge remote-tracking branch 'origin/main' into fix/agent-document-carry
ako Nov 8, 2026
666bd6d
docs(skills): mcp-server covers MCP Server 5.x and the move to model …
ako Nov 8, 2026
0659121
Merge remote-tracking branch 'origin/main' into docs/mcp-server-runti…
ako Nov 8, 2026
cce9643
Merge remote-tracking branch 'origin/main' into fix/1861-mapping-refs
ako Nov 8, 2026
db8609d
Merge remote-tracking branch 'origin/main' into fix/agent-document-carry
ako Nov 8, 2026
1c4e424
Merge pull request #2037 from ako/fix/agent-document-carry
ako Nov 8, 2026
781ed9b
Merge remote-tracking branch 'origin/main' into docs/mcp-server-runti…
ako Nov 8, 2026
4003aa1
docs(skills): mcp-server example handles its errors; tools run as the…
ako Nov 8, 2026
74fb4f0
Merge remote-tracking branch 'origin/main' into feature/2021-2022-age…
ako Nov 8, 2026
fcc216d
feat(agents): publish the app's microflows as an MCP server (#2020)
ako Nov 8, 2026
d45c930
Merge pull request #2041 from ako/docs/mcp-server-runtime-config
ako Nov 8, 2026
216e7bf
Merge remote-tracking branch 'origin/main' into feature/2021-2022-age…
ako Nov 8, 2026
520a0cc
Merge pull request #2038 from ako/feature/2021-2022-agent-settings
ako Nov 8, 2026
4d3f8aa
Merge remote-tracking branch 'origin/main' into feature/2020-publishe…
ako Nov 8, 2026
b05a4ff
Merge pull request #2044 from ako/feature/2020-published-mcp-service
ako Nov 9, 2026
9d355a2
Merge branch 'main' into fix/1861-mapping-refs
ako Nov 9, 2026
6fbef0e
Merge pull request #2019 from ako/fix/1861-mapping-refs
ako Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/backend", "date": "2026-11-08", "refs": ["ako/mxcli#2021", "ako/mxcli#2022"], "ce": [], "symptom": "`alter agent`, `alter knowledge base` and `create or modify agent|ai model|knowledge base` report success and every check stays green, but the Agent Editor document loses what mxcli does not model: per tool userAccessApproval, displayTitle, displayDescription; on the agent enableBackendStreaming, enableUIStreaming, selectedModel; on a knowledge base tool minSimilarity; the knowledge base's Contents name becomes ''; on create or modify the model's type and name become ''. create or modify agent also writes a microflow tool with no document (it runs nothing), an MCP service tool with no document, every tool with an empty id, and the entity reference with an empty documentId.", "file": "mdl/backend/modelsdk/agenteditor_carry.go (carryStoredContents, contentsSpec) wired in agenteditor_write.go writeCustomBlob; mdl/executor/cmd_agenteditor_carry.go (carryAgentIdentity, carryEntityRef); buildAgentToolFromDef in cmd_agenteditor_alter.go now shared by create; visitor toolBlockDef reads Document/ToolType. Tests: agenteditor_carry_test.go (fixtures testdata/agenteditor/*.11_16.json), cmd_agenteditor_carry_test.go", "cause": "The four Contents encoders marshal a typed struct, so a rewrite keeps only the modelled keys; the knowledge base encoder hard-coded name \"\". On the create path the visitor ignored a tool block's Document and ToolType that describe prints, the executor compared ToolType to \"mcp\" while the visitor sets \"MCP\", tools were built without ids, and the executor rebuilt Model/KB without the Portal-picked fields the statement cannot spell.", "insight": "**Measure the whole document set, byte for byte, not the reported key.** The reproduction that mattered was describe → exec of every agent-editor document of a real 11.16 app (33 in FactoryManagement, plus ICXGraphPilot's Agent Editor-created agent) and a JSON diff of Contents before/after: it surfaced the unmodelled keys, the dropped microflow target, the blanked ids, the zero-valued keys mxcli adds, and that Agent Editor 2.6 stores Contents indented by two spaces while older documents and agent-importer ones are compact — a compact rewrite of an unchanged indented document was a write every time. The carry pairs tool entries by id, so the executor must carry stored tool ids (by type + name, MCP by service) or the backend carry has nothing to pair with. A modelled optional key the statement cleared stays out (otherwise it could never be removed); an empty/zero value the stored document never held is not added. Watch the harness: `grep -v WARNING` to strip mxcli's banner also deleted a prompt line containing \"WARNING\" and looked like a describe bug. Controls: returning the rebuilt JSON fails both backend carry tests; a no-op carryAgentIdentity, dropping the model/KB carries, or disabling the microflow resolution fails each executor test with the reported symptom."}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/catalog", "date": "2026-11-08", "symptom": "CATALOG.REFS had no edge from a flow to the import or export mapping it applies and none from a mapping to its JSON structure; on FactoryManagement (11.16) 54 of 55 import mappings, 42 of 44 export mappings and 65 of 66 JSON structures had no inbound reference at all and read as dead.", "cause": "microflowActionRef returns one target per action and had no case for ImportXmlAction, ExportXmlAction, RestCallAction or WebServiceCallAction mappings; the mapping builders recorded SchemaSource but nothing projected it into refs.", "file": "mdl/catalog/builder_references.go", "insight": "A REST or web service call names a request (export) AND a response (import) mapping, so a single-target switch cannot carry it: emit a list beside it. The mapping fields are typed model.ID but hold the qualified name (BY_NAME key), so they join to *_mappings_data.QualifiedName directly; measure that with a dangling-target query (0 here) rather than trusting the type. IMPORT_MAPPING/EXPORT_MAPPING were already in RefSourceObjectTypes (member edges), which made it look as if mappings were covered. Lint output was identical (1834 with bundled Starlark rules).", "issue": "ako/mxcli#1861"}
45 changes: 36 additions & 9 deletions .claude/skills/mendix/agents/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ description: "Author Mendix AI agent documents in MDL — Model, Knowledge Base,
| lines | section |
|---|---|
| 24-50 | Overview |
| 51-139 | Syntax |
| 140-166 | Altering existing documents |
| 167-195 | Gotchas |
| 196-217 | Common Patterns |
| 218-282 | Calling Agents from Microflows |
| 283-285 | AI agent documents |
| 51-166 | Syntax |
| 167-193 | Altering existing documents |
| 194-222 | Gotchas |
| 223-244 | Common Patterns |
| 245-309 | Calling Agents from Microflows |
| 310-312 | AI agent documents |

<!-- /skill-index -->
# Agents
Expand Down Expand Up @@ -100,6 +100,22 @@ create consumed mcp service Module.WebSearch (
);
```

### Published MCP Service

The other direction: the app publishes its microflows as an MCP server
(Agent Editor 2.6+, MCP Server 5.x). `./mxcli syntax agents.published-mcp-service`
has the full syntax; the `mcp-server` skill covers the contracts of the tool and
authentication microflows and the runtime-configured alternative.

```sql
mdl 1;
create published mcp service Module.CustomerMCP (
EndpointPath: 'customers', Version: '1.0.0', ProtocolVersion: v2025_03_26
) {
tool count_customers ( Microflow: Module.MCPTool_CountCustomers, Description: 'Count customers.' )
};
```

### Agent (full syntax)

```sql
Expand Down Expand Up @@ -130,13 +146,24 @@ prompt here.$$,
Enabled: true
)

tool MyMicroflowTool (
tool FetchCustomer (
Document: Module.Tool_FetchCustomer, -- the microflow the tool runs
description: 'Fetch customer data',
Enabled: true
Enabled: true,
UserAccessApproval: UserConfirmationRequired, -- or HiddenForUser | VisibleForUser
DisplayTitle: 'Look up customer' -- shown to the user, not the model
)
};
```

Human-in-the-loop and streaming (Agent Editor 2.2+): `UserAccessApproval` and
`DisplayTitle` / `DisplayDescription` on a `tool` or `mcp service` block;
`MinSimilarity` and `IsVisible` on a `knowledge base` block;
`EnableBackendStreaming` / `EnableUIStreaming` on the **agent**, not the model.
Streaming is not shown in Studio Pro's playground; it works at runtime with a
chat agent in Conversational UI. On `create or modify`, a setting left out keeps
its stored value.

## Altering existing documents

All four agent-editor document types support `ALTER` for targeted partial updates (no need to re-specify the whole document):
Expand All @@ -154,7 +181,7 @@ alter consumed mcp service Module.MyMCP
-- Agent: SET for scalars, ADD/DROP for tools, MCP services, knowledge bases
alter agent Module.MyAgent
set SystemPrompt = 'New prompt', Temperature = 0.7, MaxTokens = 4096
add tool MyMicroflow ( Description: '...', Enabled: true )
add tool MyTool ( Document: Module.MyMicroflow, Description: '...', Enabled: true )
add mcp service Module.WeatherSvc ( Description: '...', Enabled: true )
add knowledge base Docs ( Source: Module.MyKB, Collection: 'docs', MaxResults: 5 )
drop tool OldTool
Expand Down
233 changes: 233 additions & 0 deletions .claude/skills/mendix/mcp-server/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
---
name: mcp-server
description: "Publish a Mendix app as an MCP server so external agents can call its microflows as tools and use its prompts. Covers runtime configuration with the MCP Server module's Java actions (3.x-4.x: a startup microflow that creates the server and registers tools), the authentication, tool and prompt microflow contracts, and migrating to model configuration. Use when exposing app logic over MCP or reading an existing MCP server setup. To call an external MCP server, see the agents skill."
---
<!-- skill-index: generated by scripts/skill-index.py; make check-skill-index -->
## Find it fast

`sed -n '<from>,<to>p' SKILL.md` one section rather than reading the file;
`./mxcli syntax <topic>` answers *how do I write this* for a fraction of either.

| lines | section |
|---|---|
| 43-174 | Runtime configuration |
| 175-203 | Gotchas |
| 204-233 | Reading and migrating an existing setup |

<!-- /skill-index -->
# Publishing an MCP Server

An app publishes an MCP server through the **MCP Server** marketplace module.
Two ways to configure it exist:

| | Runtime configuration | Model configuration |
|---|---|---|
| Where it lives | A microflow that runs at startup and calls the module's Java actions | A *Published MCP service* document (Agent Editor), edited in Studio Pro |
| Module versions | MCP Server 3.x and later (measured 3.0.0, 4.0.0, 4.2.2, 5.4.0) | MCP Server 5.x with Agent Editor 2.6+ (measured 5.4.0 / 2.6.0) |
| Prompts | Yes (`AddPrompt`) | No: the document holds tools only |
| MDL | `call java action MCPServer.*`, below | `create published mcp service` (agents skill) |

The two are the same mechanism. At startup AgentEditorCommons reads every
Published MCP service document and calls `MCPServer.CreateMCPServer` and
`MCPServer.AddTool` with its values (`AgentDocumentUtils.initPublishedMCPService`),
exactly as a runtime-configured app does from its own microflow. So a server
behaves the same either way, and moving between them is a 1:1 mapping (see the
end of this skill).

Runtime configuration is plain microflow logic, so everything mxcli writes for
microflows applies: `describe microflow` reads an existing setup back,
`check --references` validates it, `exec` writes it. Choose it when the server's
tools or prompts depend on data or settings known only at runtime, when it needs
prompts, or when the app is on an MCP Server version before 5.

## Runtime configuration

Four microflows: one authenticates a request, one per tool, one per prompt, and
one starts the server and registers the tools and prompts.

```sql
mdl 1;

-- Authenticate each MCP request: return the user, or empty to refuse it.
create microflow MyModule.MCP_Authenticate (
$HttpRequest: System.HttpRequest,
$McpServer: MCPServer.McpServer
)
returns System.User as $User
begin
retrieve $Headers from $HttpRequest/System.HttpHeaders;
$UserName = find $Headers by Key = 'Username';
$Password = find $Headers by Key = 'Password';
if $UserName = empty or $Password = empty then
return empty;
end if;
retrieve $User from System.User where Name = $UserName/Value first;
if $User = empty then
return empty;
end if;
-- Never let this microflow throw: an error is a 500, which also stops a client's OAuth flow.
$Valid = call java action System.VerifyPassword(userName = $UserName/Value, password = $Password/Value)
on error without rollback begin return empty; end error;
if $Valid then
return $User;
end if;
return empty;
end;

-- A tool: primitive parameters named as in the Schema, returns TextContent.
create microflow MyModule.MCPTool_CountCustomers (
$NamePrefix: String
)
returns MCPServer.TextContent as $Result
begin
retrieve $Customers from MyModule.Customer where starts-with(Name, $NamePrefix);
$Count = count $Customers;
$Result = create MCPServer.TextContent (Content = toString($Count) + ' customers');
return $Result;
end;

-- A prompt: takes MCPServer.Prompt plus its arguments, returns a PromptMessage.
create microflow MyModule.MCPPrompt_CustomerReport (
$Prompt: MCPServer.Prompt,
$CustomerName: String
)
returns MCPServer.PromptMessage as $Message
begin
$Message = create MCPServer.PromptMessage (Role = 'USER', Content = 'Write an account summary for customer ' + $CustomerName + '.');
return $Message;
end;

-- Start the server and register its tools and prompts. Call it after startup.
-- Each call handles its own error, so a server that cannot start logs and
-- returns false instead of stopping the runtime.
create microflow MyModule.MCP_Initialize ()
returns Boolean
begin
$McpServer = call java action MCPServer.CreateMCPServer(
Path = 'customers',
Name = 'Customers',
Version = '1.0.0',
ProtocolVersion = MCPServer.ENUM_ProtocolVersion.v2025_03_26,
AuthenticationMicroflow = 'MyModule.MCP_Authenticate'
) on error without rollback begin
log error node 'MCPServer' 'Could not start the Customers MCP server';
return false;
end error;
$Tool = call java action MCPServer.AddTool(
McpServer = $McpServer,
Name = 'count_customers',
Description = 'Count the customers whose name starts with NamePrefix.',
ExecutingMicroflow = 'MyModule.MCPTool_CountCustomers',
Schema = '{"type": "object", "properties": {"NamePrefix": {"type": "string"}}, "required": ["NamePrefix"]}'
) on error without rollback begin
log error node 'MCPServer' 'Could not register tool count_customers';
return false;
end error;
$Arguments = create list of MCPServer.PromptArgument;
call microflow MCPServer.PopulatePromptArgList(PromptArgumentList = $Arguments, Name = 'CustomerName', Description = 'The customer to summarize', Required = true);
$Prompt = call java action MCPServer.AddPrompt(
McpServer = $McpServer,
Name = 'customer_report',
Description = 'Draft an account summary for one customer.',
Parameters = $Arguments,
Microflow = 'MyModule.MCPPrompt_CustomerReport'
) on error without rollback begin
log error node 'MCPServer' 'Could not register prompt customer_report';
return false;
end error;
return true;
end;
```

Then call `MyModule.MCP_Initialize` from the app's after-startup microflow
(`describe settings` shows which one that is). That is how FactoryManagement
wires it; a server whose initialize microflow nothing calls is never started.

### The contracts

| Microflow | Parameters | Returns |
|---|---|---|
| Authentication | `System.HttpRequest`, optionally `MCPServer.McpServer` | `System.User`; empty refuses the request |
| Tool | Primitives named as the Schema's properties, optionally `MCPServer.Tool` | `MCPServer.TextContent` or a `String` |
| Prompt | `MCPServer.Prompt` plus one primitive per prompt argument | `MCPServer.PromptMessage` |

| Java action | Parameters | Added in 5.x |
|---|---|---|
| `MCPServer.CreateMCPServer` | `Path`, `Name`, `Version`, `ProtocolVersion`, `AuthenticationMicroflow` → `MCPServer.McpServer` | `AuthorizationServerUrl`, `Scopes` (OAuth) |
| `MCPServer.AddTool` | `McpServer`, `Name`, `Description`, `ExecutingMicroflow`, `Schema` → `MCPServer.Tool` | `Title` |
| `MCPServer.AddPrompt` | `Name`, `Description`, `Parameters` (list of `MCPServer.PromptArgument`), `Microflow`, `McpServer` → `MCPServer.Prompt` | — |

**OAuth (5.x).** `AuthorizationServerUrl` is the issuer URL of the OAuth server
that issues the tokens (e.g. `https://login.microsoftonline.com/<tenant-id>/v2.0`);
the server publishes it as Protected Resource Metadata and in its 401 responses,
so a client can find where to sign in. `Scopes` is space-separated and requires
`AuthorizationServerUrl`, which requires an authentication microflow: that
microflow must validate the access token, audience included, and return empty
rather than throw (a throw is a 500, which stops the client's OAuth flow). In
5.x the authentication microflow is optional and both its parameters are
optional; it may return a specialization of `System.User`.

A microflow parameter is passed as its qualified name in quotes, as for any Java
action. In 4.x `Schema` is optional: the module derives it from the tool
microflow's parameters when it is left empty. Write it out when the descriptions
matter to the calling model; the property names must match the parameter names.

## Gotchas

- **`ProtocolVersion` values differ per module version.** 3.0.0 has only
`v2024_11_05`; 4.0.0 and 4.2.2 have only `v2025_03_26`. Read the installed one
with `describe enumeration MCPServer.ENUM_ProtocolVersion`. The module's own
`MCPServer_Initialize` example still names `v2024_11_05` in 4.x, and is
`@excluded` for that reason: copy its shape, not its values.
- **Do not clear `AuthenticationMicroflow` after creating the server.** Some
demo apps follow `CreateMCPServer` with
`change $McpServer (AuthenticationMicroflow = empty)`, which makes the server
usable by anyone who can reach the app. With no authentication microflow the
module applies none.
- **Reserved names and paths.** 4.2.2 documents `sse` and `mcp` as reserved in
the server `Name`; 5.4 refuses a `Path` that is `mcp`, ends in `/mcp`, or
contains `/`.
- **Tools run as the authenticated user (5.4, measured).** A tool that filters
on `$currentUser` returned only that user's data over a live MCP client, so
grant each tool and prompt microflow to the roles those users hold. The 3.0
action documentation said the user was not in scope of a tool microflow.
- **`CreateMCPServer` needs the application root URL.** 5.4 builds the endpoint
from it and throws a NullPointerException when it is unset, as
`mxcli run --local` leaves it for a model holding the stock
`http://localhost:8080/`. Unhandled in an after-startup microflow, that stops
the runtime ("Starting Mendix Runtime failed"), and `mxcli test --local` with
it. Pass `--runtime-setting ApplicationRootUrl=http://localhost:8080/` to
`run --local`, and keep the `on error` handlers above so a server that cannot
start never blocks startup.
- **The client must speak the same protocol version** as `ProtocolVersion`.

## Reading and migrating an existing setup

```sql
list java actions in MCPServer;
describe enumeration MCPServer.ENUM_ProtocolVersion;
list callers of MCPServer.CreateMCPServer;
```

Each `CreateMCPServer` call is one server, and the `AddTool` / `AddPrompt` calls
on its result are its tools and prompts. That inventory is the input for a move
to model configuration. The tool microflows and the authentication microflow
carry over unchanged; the registration calls map onto the document's keys:

| Runtime configuration | Published MCP service document |
|---|---|
| `CreateMCPServer(Path)` | `endpointPath` |
| `Name` / `Version` / `ProtocolVersion` | `serverName` / `version` / `protocolVersion` |
| `AuthenticationMicroflow` | `authenticationEnabled: true` + `authenticationMicroflow` |
| `AuthorizationServerUrl` / `Scopes` | `authorizationServerUrl` / `scopes` (read only when authentication is enabled) |
| each `AddTool(Name, Description, ExecutingMicroflow, Schema, Title)` | a `tools` entry: `name`, `description`, `sourceMicroflow`, `schema`, `title` |
| `AddPrompt` | no equivalent: keep a runtime microflow for prompts |

After the move, remove the initialize call from the after-startup microflow, so
the server is not created twice. Write the document with
`create published mcp service` (`./mxcli syntax agents.published-mcp-service`):
`EndpointPath`, `Version`, `ProtocolVersion`, `AuthenticationEnabled`,
`AuthenticationMicroflow`, `AuthorizationServerUrl` and `Scopes` take the
values of the `CreateMCPServer` call (its `Name` is `ServerName`, which defaults
to the document name), and each `AddTool` becomes a
`tool name ( Microflow: …, Description: …, Title: …, Schema: … )` block.
Loading
Loading