Skip to main content
Glama
dhanavel-013

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.
```