mendix-mcp-server
by dhanavel-013
README.md
# Mendix MCP Server
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that exposes the
**Mendix Model SDK**, **Mendix Platform API**, **Deploy API**, and a running app's
**runtime OQL endpoint** as a suite of tools an AI agent (e.g. Claude Code) can call.
In short: it lets an LLM *read and modify a Mendix application model* — create modules,
entities, attributes, associations, pages, microflows, REST services, security rules,
navigation, and enumerations — and then commit, deploy, or query the result, all through
plain tool calls over HTTP.
---
## Table of contents
- [How it works](#how-it-works)
- [Architecture](#architecture)
- [The four Mendix surfaces it talks to](#the-four-mendix-surfaces-it-talks-to)
- [The working-copy lifecycle (the core pattern)](#the-working-copy-lifecycle-the-core-pattern)
- [Request lifecycle](#request-lifecycle)
- [Setup](#setup)
- [Configuration](#configuration)
- [Running the server](#running-the-server)
- [Connecting an MCP client](#connecting-an-mcp-client)
- [Tool catalog](#tool-catalog)
- [Project layout](#project-layout)
- [How a typical task flows end-to-end](#how-a-typical-task-flows-end-to-end)
- [Notes, limitations & gotchas](#notes-limitations--gotchas)
---
## How it works
Mendix apps are not edited as loose files. The model lives on Mendix's **Team Server**, and
the canonical way to change it programmatically is the **Mendix Model SDK** (`mendixmodelsdk`)
driven through the **Platform SDK** (`mendixplatformsdk`). The pattern is always:
1. Open a **temporary online working copy** of an app from a branch.
2. Open its **model** (a live, typed, in-memory object graph of the whole app).
3. Read or mutate model elements (entities, pages, microflows, …).
4. **Flush** pending changes and **commit** the working copy back to the branch.
This server wraps that pattern in ~60 discrete MCP tools. Each tool is a thin, focused
operation ("add an attribute to an entity", "add a widget to a page", "create a REST
operation") that:
- takes an `app_id` (and usually a `branch`, defaulting to `main`),
- opens a fresh working copy + model,
- performs one well-scoped change,
- commits with a generated message,
- and returns a human/JSON result.
The AI agent orchestrates these small tools into larger changes. The server itself stays
stateless — it holds no session between calls.
---
## Architecture
```
┌───────────────────┐ JSON-RPC over HTTP (POST /mcp) ┌────────────────────────┐
│ MCP client │ ───────────────────────────────────────► │ Mendix MCP Server │
│ (Claude Code, etc.)│ ◄─────────────────────────────────────── │ (Node.js, port 3001) │
└───────────────────┘ tool results / streamed events └───────────┬────────────┘
│
┌──────────────────────────────────────────────────────┼───────────────────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────┐ ┌────────────────────┐
│ Platform SDK │ │ Deploy API (v2) │ │ Runtime OQL endpoint │ │ Local FS / Web │
│ + Model SDK │ │ deploy.mendix.com │ │ /rest/oql/v1/query │ │ (glob/read/write/ │
│ (Team Server model)│ │ │ │ on the running app │ │ fetch/docs search)│
└────────────────────┘ └──────────────────────────┘ └──────────────────────┘ └────────────────────┘
▲ ▲ ▲
MENDIX_PAT MENDIX_USERNAME + MENDIX_RUNTIME_TOKEN
MENDIX_API_KEY
```
Two source files form the spine:
- [`src/server.ts`](src/server.ts) — the HTTP + MCP transport layer. Builds an `McpServer`,
registers every tool group, and serves them over a **Streamable HTTP** transport at
`POST /mcp`.
- [`src/mendix-client.ts`](src/mendix-client.ts) — the shared Mendix helper layer:
authentication, working-copy/model lifecycle, module/document lookup, multi-language text
handling, and result formatting.
Everything else lives under [`src/tools/`](src/tools/), one file per capability area, each
exporting a `registerXxxTools(server)` function that `server.ts` calls.
---
## The four Mendix surfaces it talks to
The server does **not** hit a single Mendix API. Different tools use different Mendix
surfaces, each with its own credential:
| Surface | Used by | Credential | What it does |
|---|---|---|---|
| **Model SDK / Platform SDK** | Most tools — domain, pages, flows, security, navigation, enums, REST modeling, `ped_*`, `project_commit/branch/export/settings` | `MENDIX_PAT` | Opens working copies and reads/mutates the app model on the Team Server. |
| **Deploy API v2** (`deploy.mendix.com`) | `project_deploy`, `project_get_deploy_status` | `MENDIX_USERNAME` + `MENDIX_API_KEY` | Triggers cloud deployments and reports their status. |
| **Runtime OQL endpoint** (`/rest/oql/v1/query`) | `oql_read` | `MENDIX_RUNTIME_TOKEN` | Runs OQL against a *live, running* app instance and returns data. |
| **Local FS / public web** | `glob`, `read_file`, `write_file`, `read_skill`, `web_fetch`, `search_mendix_knowledge_base`, `oql_generate` | none | Local file ops, doc search, and pure string generation — no Mendix auth needed. |
---
## The working-copy lifecycle (the core pattern)
Almost every model-mutating tool follows the same shape, implemented via helpers in
[`src/mendix-client.ts`](src/mendix-client.ts):
```ts
// 1. Open a fresh working copy for the app/branch and open its model.
const wc = await getWorkingCopy(app_id, branch); // createTemporaryWorkingCopy(branch)
const model = await wc.openModel();
// 2. Locate the target element.
const mod = findModule(model, moduleName); // by name
const doc = findDocument(model, moduleName, docName); // recursive folder search
// 3. Mutate the in-memory model graph (create/update/delete SDK objects).
// ...
// 4. Flush pending changes and commit the working copy back to the branch.
await commitWC(wc, model, "descriptive commit message", branch);
```
Read-only tools skip step 4 and just call `getModel(app_id, branch)` to open a throwaway
working copy for inspection.
Key helpers in `mendix-client.ts`:
- `mendix` — the singleton `MendixPlatformClient`. Its PAT is bridged from `MENDIX_PAT` into
the SDK's expected `MENDIX_TOKEN` mechanism via `setPlatformConfig(...)`.
- `getWorkingCopy(appId, branch)` / `getModel(appId, branch)` — open a working copy / model.
- `commitWC(wc, model, message, branch)` — `flushChanges()` then `commitToRepository(...)`.
- `findModule` / `findDocument` / `splitQualifiedName` — resolve `Module.Element` names,
searching recursively through folders.
- `setText` / `readText` / `getProjectLanguageCodes` — Mendix user-visible text is never a
plain string; it's a `Text` holding one `Translation` per configured language. `setText`
writes a translation for **every** project language (avoiding consistency errors) and caches
the language list per model.
- `ok(msg)` / `json(data)` / `errorResult(err)` — normalize tool return payloads.
- `safe(handler)` — wraps every tool handler so a thrown error becomes an MCP error result
(`isError: true`) instead of crashing the server.
---
## Request lifecycle
The HTTP layer in [`src/server.ts`](src/server.ts) runs in **stateless mode**:
1. A client POSTs a JSON-RPC MCP message to `http://localhost:3001/mcp`.
2. The body is read and parsed.
3. A **fresh** `McpServer` + `StreamableHTTPServerTransport` pair is built **per request**.
(An MCP server/transport pair can only be `connect()`-ed once — reusing it throws on the
second call — so the server builds a new pair each time rather than holding a session.)
4. All tool groups are registered on that fresh server, it connects to the transport, and the
transport handles the request/response.
5. On connection close, both transport and server are closed.
Any non-`/mcp` path returns `404`. Errors are returned as JSON-RPC error objects
(`code: -32603`).
---
## Setup
### Prerequisites
- **Node.js** (with support for ES modules / `NodeNext`; Node 18+ recommended for global
`fetch`).
- A **Mendix account** with:
- a **Personal Access Token (PAT)** with `mx:modelrepository` scopes (for model editing),
- optionally an **API key + username** (for deployments),
- optionally a **runtime token** (for live OQL queries).
- The Mendix app's **App ID** (found in the Mendix Portal / Developer Portal).
### Install
```bash
npm install
```
---
## Configuration
Copy [`.env.example`](.env.example) to `.env` and fill in your credentials:
```dotenv
MENDIX_PAT=your_personal_access_token # Model/Platform SDK — model editing
MENDIX_USERNAME=your@email.com # Deploy API — deployments
MENDIX_API_KEY=your_deploy_api_key # Deploy API — deployments
MENDIX_RUNTIME_TOKEN=your_runtime_token # Runtime OQL — live data queries
PORT=3001 # HTTP port (default 3001)
```
`.env` is loaded automatically at startup via `dotenv/config`. Each credential is only
required for the tools that use it — you can run model-editing tools with just `MENDIX_PAT`.
---
## Running the server
```bash
# Type-check + compile TypeScript → dist/
npm run build
# Run the compiled server
npm start
# → Mendix MCP -> http://localhost:3001/mcp
# Or run directly from source during development (ts-node loader):
npm run dev
# Auto-restart on file changes:
npm run watch
```
---
## Connecting an MCP client
Point any MCP client at the HTTP endpoint. For **Claude Code**, add an HTTP MCP server:
```bash
claude mcp add --transport http mendix http://localhost:3001/mcp
```
Or via a client config that supports Streamable HTTP transports, use the URL
`http://localhost:3001/mcp`. Once connected, the client discovers all tools below and the
agent can call them by name.
---
## Tool catalog
All tools that touch the model accept `app_id` and (usually) an optional `branch` (default
`main`). Grouped by source file:
### Filesystem & knowledge — [`filesystem.ts`](src/tools/filesystem.ts), [`knowledge.ts`](src/tools/knowledge.ts)
| Tool | Description |
|---|---|
| `glob` | Find files matching a glob pattern. |
| `read_file` | Read a file's contents. |
| `write_file` | Write or overwrite a file. |
| `read_skill` | Read a skill/snippet from the skills library. |
| `web_fetch` | Fetch content from a URL. |
| `search_mendix_knowledge_base` | Search Mendix docs / knowledge base (docs.mendix.com). |
### Platform / element document ops (`ped_*`) — [`ped.ts`](src/tools/ped.ts)
Generic, low-level building blocks used by the higher-level tools.
| Tool | Description |
|---|---|
| `ped_read_document` | Read a document's full JSON representation. |
| `ped_get_schema` | Summarize the SDK class + common properties for a document type. |
| `ped_create_document` | Create a Page, Microflow, Nanoflow, Enumeration, or Snippet. |
| `ped_create_module` | Create a new module. |
| `ped_update_document` | Apply key-value property updates to a document. |
| `ped_check_errors` | Run consistency checks on the model. |
| `ped_find_document` | Search documents by name substring / module / type. |
| `ped_list_folder` | List documents directly inside a module (or subfolder). |
### Domain model — [`domain.ts`](src/tools/domain.ts)
| Tool | Description |
|---|---|
| `entity_create` | Create an entity (persistence via generalization; see note below). |
| `entity_add_attribute` | Add an attribute (optional default value + required rule). |
| `entity_update_attribute` | Apply property updates to an attribute. |
| `entity_delete_attribute` | Delete an attribute. |
| `entity_list_attributes` | List an entity's attributes with names + types. |
| `entity_set_access_rules` | Set entity access rules. |
| `entity_copy` | Copy an entity (including attributes). |
| `entity_delete` | Delete an entity. |
| `association_create` | Create an association between two entities (cross-module OK). |
| `association_set_properties` | Apply property updates to an association. |
| `domain_model_export` | Export a curated summary of a module's domain model. |
Supported attribute types: `String, Integer, Long, Decimal, Boolean, DateTime, Enum,
AutoNumber, Binary, HashString`.
### Enumerations, constants & snippets — [`enums.ts`](src/tools/enums.ts)
| Tool | Description |
|---|---|
| `enum_add_value` / `enum_update_value` / `enum_list_values` | Manage enumeration values. |
| `constant_create` / `constant_set_value` | Create/set a Constant document. |
| `snippet_create` / `snippet_add_to_page` | Create a reusable snippet and place it on a page. |
### Pages & widgets — [`pages.ts`](src/tools/pages.ts)
| Tool | Description |
|---|---|
| `page_add_widget` / `page_remove_widget` / `page_move_widget` | Add/remove/reposition widgets. |
| `page_set_widget_property` | Set a single widget property (dynamic key-value). |
| `page_set_data_source` | Wire a widget's data source. |
| `page_set_conditional_visibility` | Set a visibility expression on a widget. |
| `page_set_layout` | Change the page's layout. |
| `page_add_listview` | Add a ListView wired to an entity. |
| `page_add_datagrid` | Add a DataGrid wired to an entity, one column per attribute. |
| `page_set_class` | Set a widget's CSS class. |
| `page_duplicate` | Duplicate a page. |
| `page_set_navigation_target` | Make an ActionButton navigate to another page. |
| `page_set_button_caption` | Set a button's caption. |
### Microflows / nanoflows — [`flows.ts`](src/tools/flows.ts)
| Tool | Description |
|---|---|
| `flow_add_activity` | Add an activity node. |
| `flow_add_decision` | Add an exclusive split with an expression condition. |
| `flow_connect_activities` | Connect nodes with flows. |
| `flow_configure_activity` | Configure an activity's properties. |
| `flow_add_variable` | Add a create-variable activity. |
| `flow_add_retrieve` | Add a database retrieve (list by entity). |
| `flow_add_commit` | Add a commit-object activity. |
| `flow_add_loop` | Add a loop over a list variable (empty body). |
| `flow_add_error_handler` | Add an error handler. |
| `flow_call_microflow` | Call another microflow with argument mappings. |
| `flow_call_rest_service` | Add a call-REST-service activity. |
| `flow_get_activities` | List all nodes (id, type, position). |
| `flow_run_microflow` | Run a microflow. |
### REST services — [`rest.ts`](src/tools/rest.ts)
| Tool | Description |
|---|---|
| `rest_create_consumed_service` / `rest_add_operation` | Model a consumed REST service + operations. |
| `rest_create_published_service` / `rest_add_published_resource` | Model a published REST service at a base path. |
| `rest_import_openapi` | Import an OpenAPI/Swagger spec (pragmatic, non-`$ref` parser) into a consumed service. |
| `rest_list_services` | List all consumed + published services. |
| `rest_set_mapping` | Configure a mapping. |
| `rest_test_operation` | Invoke an operation for testing. |
### Security — [`security.ts`](src/tools/security.ts)
| Tool | Description |
|---|---|
| `security_create_userrole` / `security_list_roles` | Manage project user roles. |
| `security_set_page_access` | Set module roles allowed on a page. |
| `security_set_microflow_access` | Set module roles allowed on a microflow. |
| `security_set_entity_access` | Set entity access rules. |
| `security_get_access_summary` | Summarize access configuration. |
### Navigation — [`navigation.ts`](src/tools/navigation.ts)
| Tool | Description |
|---|---|
| `navigation_get_menu` | Read a navigation profile's menu. |
| `navigation_add_menu_item` / `navigation_remove_menu_item` | Add/remove menu items. |
| `navigation_reorder_items` | Reorder menu items. |
| `navigation_set_home_page` | Set the profile's home page. |
### OQL — [`oql.ts`](src/tools/oql.ts)
| Tool | Description | Auth |
|---|---|---|
| `oql_generate` | Build an OQL string from parameters (local templating). | none |
| `oql_read` | Run OQL against a live running app (`POST /rest/oql/v1/query`). | `MENDIX_RUNTIME_TOKEN` |
### Deployment & project — [`deployment.ts`](src/tools/deployment.ts)
| Tool | Description | Auth |
|---|---|---|
| `project_commit` | Open a working copy and commit (checkpoint history). | `MENDIX_PAT` |
| `project_create_branch` | Create a branch. | `MENDIX_PAT` |
| `project_get_revisions` | Get a branch's commit history (Team Server API). | `MENDIX_PAT` |
| `project_deploy` | Deploy to a cloud environment (Sandbox/Test/Acceptance/Production). | `MENDIX_USERNAME` + `MENDIX_API_KEY` |
| `project_get_deploy_status` | Get a deployment's status. | `MENDIX_USERNAME` + `MENDIX_API_KEY` |
| `project_export_mpk` | Export the model as a local `.mpk`. | `MENDIX_PAT` |
| `project_get_settings` | Get the app's project settings document. | `MENDIX_PAT` |
---
## Project layout
```
mendix-mcp-server/
├── src/
│ ├── server.ts # HTTP + MCP transport; registers all tool groups
│ ├── mendix-client.ts # Auth, working-copy/model lifecycle, helpers
│ └── tools/
│ ├── filesystem.ts # glob / read / write / fetch / read_skill
│ ├── knowledge.ts # Mendix docs search
│ ├── ped.ts # generic document/module ops (create/read/update/errors)
│ ├── domain.ts # entities, attributes, associations
│ ├── enums.ts # enumerations, constants, snippets
│ ├── pages.ts # pages & widgets
│ ├── flows.ts # microflows / nanoflows
│ ├── rest.ts # consumed & published REST services, OpenAPI import
│ ├── security.ts # user roles, page/microflow/entity access
│ ├── navigation.ts # navigation menus
│ ├── oql.ts # OQL generate + live read
│ └── deployment.ts # commit, branch, deploy, export, settings
├── dist/ # compiled output (npm run build)
├── package.json
├── tsconfig.json
├── .env / .env.example # credentials & port
└── README.md
```
---
## How a typical task flows end-to-end
Say the agent is asked: *"Add a `Customer` entity to the `Sales` module with a `Name` field,
put it on a data grid, and deploy to Sandbox."*
1. **`entity_create`** → opens a working copy of the app, creates the `Customer` entity in
`Sales`, commits.
2. **`entity_add_attribute`** → opens a fresh working copy, adds `Name` (String), commits.
3. **`ped_create_document`** → creates a `CustomerOverview` Page.
4. **`page_add_datagrid`** → wires a DataGrid to `Sales.Customer` with a column per attribute,
commits.
5. **`navigation_add_menu_item`** → links the page into the navigation menu.
6. **`ped_check_errors`** → runs consistency checks to confirm the model is valid.
7. **`project_deploy`** → hits the Deploy API to push to the Sandbox environment.
8. **`project_get_deploy_status`** → polls until the deployment reports success.
Each step is a separate stateless HTTP call; the agent decides the order and reacts to each
tool's result.
---
## Notes, limitations & gotchas
- **Stateless by design.** No sessions are kept; each request builds and tears down its own
MCP server/transport pair. This is intentional — the MCP SDK forbids reusing a
connected pair.
- **Every mutating call re-opens a working copy.** Because tools are independent, a multi-step
change is a series of commits, not one atomic transaction. Use `project_commit` to checkpoint
and `ped_check_errors` to validate along the way.
- **Text is multi-language.** Any caption/label/title is a `Text` with one `Translation` per
project language. Always use the `setText`/`readText` helpers (the higher-level tools do this
for you) — don't assume a plain string.
- **Entity persistence** is expressed through the entity's *generalization*, not a standalone
flag: a `NoGeneralization` with `persistable=true/false`, or a `Generalization` pointing at a
parent entity (which then determines persistence).
- **`rest_import_openapi` is pragmatic, not complete.** It reads `info.title`, the server URL,
and `paths.*.<method>.operationId`, but does **not** resolve `$ref`s or import
parameter/response schemas.
- **Credential scoping.** Model tools need only `MENDIX_PAT`; deploy tools additionally need
`MENDIX_USERNAME` + `MENDIX_API_KEY`; `oql_read` needs `MENDIX_RUNTIME_TOKEN`. Missing
credentials produce a clear error only when the relevant tool is called.
- **Errors are non-fatal.** The `safe()` wrapper converts thrown errors into MCP error results,
so a failing tool call returns an error payload rather than taking the server down.
```
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues