diff --git a/package.json b/package.json
index cc027c26c4..15bfe029cf 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"create-gh-release": "turbo run create-gh-release --concurrency 1",
"create-translation": "turbo run create-translation",
"postinstall": "turbo run agent-rules",
+ "start:mcp": "pnpm --filter pluggable-widgets-mcp run start",
"lint": "turbo run lint --continue --concurrency 1",
"prepare": "husky install",
"prepare-release": "pnpm --filter @mendix/automation-utils run prepare-release",
diff --git a/packages/pluggable-widgets-mcp/.gitignore b/packages/pluggable-widgets-mcp/.gitignore
new file mode 100644
index 0000000000..e6cd8c7e6d
--- /dev/null
+++ b/packages/pluggable-widgets-mcp/.gitignore
@@ -0,0 +1,3 @@
+dist/
+generations/
+node_modules/
\ No newline at end of file
diff --git a/packages/pluggable-widgets-mcp/.prettierrc.cjs b/packages/pluggable-widgets-mcp/.prettierrc.cjs
new file mode 100644
index 0000000000..0892704ab0
--- /dev/null
+++ b/packages/pluggable-widgets-mcp/.prettierrc.cjs
@@ -0,0 +1 @@
+module.exports = require("@mendix/prettier-config-web-widgets");
diff --git a/packages/pluggable-widgets-mcp/AGENTS.md b/packages/pluggable-widgets-mcp/AGENTS.md
new file mode 100644
index 0000000000..9b34567841
--- /dev/null
+++ b/packages/pluggable-widgets-mcp/AGENTS.md
@@ -0,0 +1,113 @@
+# Pluggable Widgets MCP Server
+
+MCP server enabling AI assistants to scaffold and manage Mendix pluggable widgets via STDIO (default) or HTTP transport.
+
+## Quick Reference
+
+```bash
+pnpm dev # Development with hot reload
+pnpm build # TypeScript compilation + path alias resolution
+pnpm start # Build and run (STDIO mode, default)
+pnpm start:http # Build and run (HTTP mode, port 3100)
+pnpm lint # ESLint check
+```
+
+## Project Structure
+
+```
+src/
+├── index.ts # Entry point - transport mode selection
+├── config.ts # Server configuration and constants
+├── security/ # Path traversal & extension validation
+├── server/ # HTTP and STDIO transport setup
+├── resources/ # MCP resources (guidelines)
+├── generators/ # XML and TSX code generators
+└── tools/ # MCP tool implementations
+```
+
+## Adding Tools
+
+1. Create `src/tools/my-feature.tools.ts`:
+
+```typescript
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { z } from "zod";
+
+export function registerMyTools(server: McpServer): void {
+ server.tool(
+ "my-tool",
+ "Description shown to LLM",
+ z.object({ param: z.string().describe("Parameter description") }),
+ async ({ param }) => ({
+ content: [{ type: "text", text: "Success" }]
+ })
+ );
+}
+```
+
+2. Register in `src/tools/index.ts`:
+
+```typescript
+import { registerMyTools } from "./my-feature.tools";
+
+export function registerAllTools(server: McpServer): void {
+ // ... existing registrations
+ registerMyTools(server);
+}
+```
+
+## Code Patterns
+
+- **Imports**: Use `@/` path alias for absolute imports from `src/`
+- **Schemas**: All tool inputs require Zod schemas
+- **Errors**: Use `createErrorResponse()` from `@/tools/utils/response`
+- **Long operations**: Use `ProgressTracker` from `@/tools/utils/progress-tracker`
+
+## Notification Behavior (Important for AI Agents)
+
+When using this MCP server, understand where different types of output appear:
+
+| Output Type | Visibility | Purpose |
+| -------------------------- | -------------------------- | ------------------------------------------------------- |
+| **Tool Results** | ✅ Visible in conversation | Final outcomes, structured data, success/error messages |
+| **Progress Notifications** | ❌ Not in conversation | Client UI indicators only (spinners, progress bars) |
+| **Log Messages** | ❌ Not in conversation | Debug console/MCP Inspector only |
+
+**Key Implications for AI Agents:**
+
+1. **Don't expect intermediate progress in chat**: Long operations (scaffolding, building) will show results only when complete. The conversation won't contain step-by-step progress updates.
+
+2. **Tool results are authoritative**: Only tool result content appears in the conversation history. Use this for:
+
+ - Success confirmations with file paths
+ - Structured error messages with suggestions
+ - Any information the AI needs to continue the workflow
+
+3. **Progress tracking is for humans**: `sendProgress()` and `sendLogMessage()` are for human observers using MCP Inspector or UI indicators, not for AI decision-making.
+
+4. **When debugging**:
+ - If operations seem to "hang", check MCP Inspector's Notifications/Logs panels
+ - Progress notifications confirm the server is working, even if the chat is quiet
+ - This is per MCP specification, not a bug
+
+**Example Workflow:**
+
+```typescript
+// ❌ This progress won't appear in AI's context
+await sendProgress(context, 50, "Scaffolding widget...");
+
+// ✅ This result WILL appear in AI's context
+return createToolResponse(`Widget created at ${widgetPath}`);
+```
+
+## Testing
+
+```bash
+npx @modelcontextprotocol/inspector node dist/index.js
+```
+
+## Security
+
+**Read before implementing file operations**: [docs/agent/security.md](docs/agent/security.md)
+
+All file operation tools must use `validateFilePath()` from `@/security` to prevent path traversal attacks.
diff --git a/packages/pluggable-widgets-mcp/CHANGELOG.md b/packages/pluggable-widgets-mcp/CHANGELOG.md
new file mode 100644
index 0000000000..8c63b9710f
--- /dev/null
+++ b/packages/pluggable-widgets-mcp/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+All notable changes to this MCP server will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- We introduce pluggable-widgets-mcp.
diff --git a/packages/pluggable-widgets-mcp/README.md b/packages/pluggable-widgets-mcp/README.md
new file mode 100644
index 0000000000..39ac713ce2
--- /dev/null
+++ b/packages/pluggable-widgets-mcp/README.md
@@ -0,0 +1,267 @@
+# Mendix Pluggable Widgets MCP Server
+
+> **Work in Progress** - This is an MVP focused on widget scaffolding. Widget editing capabilities coming soon.
+
+A Model Context Protocol (MCP) server that enables AI assistants to scaffold Mendix pluggable widgets programmatically.
+
+## Quick Start
+
+```bash
+pnpm install
+pnpm build # Build the server
+pnpm start # STDIO mode (default)
+pnpm start:stdio # HTTP mode
+```
+
+## Global Installation
+
+For use with MCP clients (Cursor, Claude Desktop, LMStudio), install globally:
+
+```bash
+# Build first
+pnpm build
+
+# Link globally using npm (NOT pnpm - better MCP client compatibility)
+npm link
+
+# Verify installation
+which pluggable-widgets-mcp
+```
+
+## Transport Modes
+
+### STDIO Mode (default)
+
+Runs via stdin/stdout for CLI-based MCP clients (Claude Desktop, etc.).
+
+```bash
+pnpm start
+pnpm start:stdio
+```
+
+### HTTP Mode
+
+Runs an HTTP server for web-based MCP clients.
+
+```bash
+pnpm start:http
+```
+
+- Server runs on `http://localhost:3100` (override with `PORT` env var)
+- Health check: `GET /health`
+- MCP endpoint: `POST /mcp`
+
+## MCP Client Configuration
+
+### HTTP
+
+```json
+{
+ "mcpServers": {
+ "pluggable-widgets-mcp": {
+ "url": "http://localhost:3100/mcp"
+ }
+ }
+}
+```
+
+### STDIO
+
+**_Some client setups like Claude Desktop support STDIO only (for now)_**
+
+**Option 1: Global command (after `npm link`)**
+
+```json
+{
+ "mcpServers": {
+ "pluggable-widgets-mcp": {
+ "command": "pluggable-widgets-mcp",
+ "args": ["stdio"]
+ }
+ }
+}
+```
+
+**Option 2: Absolute path (more reliable during development)**
+
+```json
+{
+ "mcpServers": {
+ "pluggable-widgets-mcp": {
+ "command": "node",
+ "args": ["/path/to/pluggable-widgets-mcp/dist/index.js", "stdio"]
+ }
+ }
+}
+```
+
+> **Note:** After rebuilding the server, you may need to restart/reconnect your MCP client to pick up changes.
+
+## Available Tools
+
+### create-widget
+
+Scaffolds a new Mendix pluggable widget using `@mendix/generator-widget`.
+
+| Parameter | Required | Default | Description |
+| --------------------- | -------- | ------------ | ------------------------------------ |
+| `name` | Yes | - | Widget name (PascalCase recommended) |
+| `description` | Yes | - | Brief description of the widget |
+| `version` | No | `1.0.0` | Initial version (semver) |
+| `author` | No | `Mendix` | Author name |
+| `license` | No | `Apache-2.0` | License type |
+| `organization` | No | `Mendix` | Organization namespace |
+| `template` | No | `empty` | `full` (sample code) or `empty` |
+| `programmingLanguage` | No | `typescript` | `typescript` or `javascript` |
+| `unitTests` | No | `true` | Include unit test setup (Jest/TS) |
+| `e2eTests` | No | `false` | Include E2E test setup (Playwright) |
+
+Generated widgets are placed in `generations/` directory within this package.
+
+### File Operation Tools
+
+| Tool | Description |
+| -------------------------- | ------------------------------------------------------------ |
+| `list-widget-files` | Lists all files in a widget directory, grouped by type |
+| `read-widget-file` | Reads the contents of a file from a widget directory |
+| `write-widget-file` | Writes content to a file (creates parent dirs automatically) |
+| `batch-write-widget-files` | Writes multiple files atomically |
+
+**Security:** All file operations are protected by `src/security/guardrails.ts`:
+
+- Path traversal is blocked (no `..` escapes)
+- Extension whitelist: `.tsx`, `.ts`, `.xml`, `.scss`, `.css`, `.json`, `.md`
+
+### build-widget
+
+Builds a widget using `pluggable-widgets-tools`, producing an `.mpk` file.
+
+| Parameter | Required | Description |
+| ------------ | -------- | ------------------------------------- |
+| `widgetPath` | Yes | Absolute path to the widget directory |
+
+Returns structured errors for TypeScript, XML, or dependency issues.
+
+## Available Resources
+
+| URI | Description |
+| ------------------------------------- | -------------------------------------------------------------------------- |
+| `mendix://guidelines/property-types` | Complete reference for all Mendix widget property types |
+| `mendix://guidelines/widget-patterns` | Reusable patterns for common widget types (Button, Input, Container, etc.) |
+
+## Development
+
+```bash
+pnpm dev # Development mode with hot reload
+pnpm build # Build for production
+pnpm start # Build and run
+```
+
+## Testing with MCP Inspector
+
+The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is an interactive debugging tool for testing MCP servers. It provides a web UI to connect to your server, explore available tools, and execute them with custom inputs.
+
+### Quick Start
+
+```bash
+# Run Inspector against this server (STDIO mode)
+npx @modelcontextprotocol/inspector node dist/index.js stdio
+
+# Or for HTTP mode, start the server first then connect via Inspector
+pnpm start
+npx @modelcontextprotocol/inspector
+# Then enter http://localhost:3100/mcp as the server URL
+```
+
+### Using the Inspector
+
+1. **Connect** - The Inspector will automatically connect to your MCP server
+2. **Explore Tools** - View all registered tools (`create-widget`, etc.) with their schemas
+3. **Execute Tools** - Fill in parameters and run tools to test behavior
+4. **View Responses** - See JSON responses, progress notifications, and logs in real-time
+
+### Example: Testing `create-widget`
+
+1. Start the Inspector: `npx @modelcontextprotocol/inspector node dist/index.js stdio`
+2. Select the `create-widget` tool from the tools list
+3. Fill in required parameters:
+ ```json
+ {
+ "name": "TestWidget",
+ "description": "A test widget",
+ ... // Defaults for other optional values if not entered
+ }
+ ```
+4. Click "Execute" and watch progress notifications as the widget is scaffolded
+5. Check `generations/testwidget/` for the created widget
+
+This is useful for verifying tool behavior without needing a full AI client integration.
+
+## Understanding Feedback and Notifications
+
+This server uses MCP's notification system to provide progress updates and logging. However, **different types of feedback appear in different places**—not all feedback shows up in your chat conversation.
+
+### Where Different Types of Feedback Appear
+
+| Feedback Type | Where It Appears | Example |
+| -------------------------- | -------------------------------------- | ----------------------------------------------------------------- |
+| **Tool Results** | ✅ Chat conversation | Widget created at `/path/to/widget`, Build completed successfully |
+| **Progress Notifications** | ⚙️ Client UI (spinners, progress bars) | "Scaffolding widget...", "Building widget..." |
+| **Log Messages** | 🔍 Debug console (MCP Inspector) | Detailed operation logs, debug info |
+
+### Why Progress Doesn't Show in Chat
+
+**This is by design per the MCP specification**, not a bug. The MCP architecture separates concerns:
+
+- **`notifications/progress`** → Routed to client UI indicators (loading spinners, status bars)
+- **`notifications/message`** → Routed to debug/inspector consoles for developers
+- **Tool results** → Returned to the conversation when operations complete
+
+This means:
+
+- Long operations (scaffolding, building) will show **results** when complete
+- You won't see intermediate progress steps in the chat history
+- MCP Inspector shows all notifications in real-time (bottom-right panel)
+
+### Viewing Debug Output
+
+**With MCP Inspector:**
+
+1. Run: `npx @modelcontextprotocol/inspector node dist/index.js stdio`
+2. Execute a tool (e.g., `create-widget`)
+3. Watch the **Notifications panel** (bottom-right) for progress updates
+4. Check the **Logs panel** for detailed debug output
+
+**With Claude Desktop:**
+
+- Progress notifications may appear as UI indicators (client-dependent)
+- Check Claude Desktop's developer console for log messages (if available)
+- Tool results will always appear in the conversation
+
+### Expected Behavior Examples
+
+**During widget scaffolding:**
+
+- Chat shows: "Starting scaffolding..." → (wait) → "Widget created at `/path`"
+- Inspector shows: Step-by-step progress notifications for all 14 prompts
+
+**During widget building:**
+
+- Chat shows: "Building..." → (wait) → "Build successful" or structured error
+- Inspector shows: TypeScript compilation progress, dependency resolution
+
+## Roadmap
+
+- [x] Widget scaffolding (`create-widget`)
+- [x] HTTP transport
+- [x] STDIO transport
+- [x] Progress notifications
+- [x] File operations (list, read, write, batch-write)
+- [x] Build tool (`build-widget`)
+- [x] Guideline resources (property-types, widget-patterns)
+- [ ] Widget property editing (XML manipulation)
+- [ ] TypeScript error recovery suggestions
+
+## License
+
+Apache-2.0 - Mendix Technology BV 2025
diff --git a/packages/pluggable-widgets-mcp/docs/agent/security.md b/packages/pluggable-widgets-mcp/docs/agent/security.md
new file mode 100644
index 0000000000..816e26ff25
--- /dev/null
+++ b/packages/pluggable-widgets-mcp/docs/agent/security.md
@@ -0,0 +1,20 @@
+# Security
+
+All security validation is centralized in `src/security/guardrails.ts`:
+
+```typescript
+import { validateFilePath, ALLOWED_EXTENSIONS } from "@/security";
+
+// Validates path traversal and extension whitelist
+validateFilePath(widgetPath, filePath, true); // true = check extension
+```
+
+## Security Measures
+
+| Protection | Function | Description |
+| ------------------- | ------------------------- | ------------------------------------------------------------------- |
+| Path Traversal | `validateFilePath()` | Blocks `..` sequences and resolved path escapes |
+| Extension Whitelist | `isExtensionAllowed()` | Only allows: `.tsx`, `.ts`, `.xml`, `.scss`, `.css`, `.json`, `.md` |
+| Directory Boundary | `isPathWithinDirectory()` | Ensures files stay within widget directory |
+
+**Rule**: When adding file operation tools, always use `validateFilePath()` from the security module.
diff --git a/packages/pluggable-widgets-mcp/docs/property-types.md b/packages/pluggable-widgets-mcp/docs/property-types.md
new file mode 100644
index 0000000000..c2936c7890
--- /dev/null
+++ b/packages/pluggable-widgets-mcp/docs/property-types.md
@@ -0,0 +1,588 @@
+# Mendix Widget Property Types Reference
+
+This document defines all available property types for Mendix pluggable widgets. Use this reference when defining properties in the JSON schema for XML generation.
+
+## Property Definition Schema
+
+When defining properties for the XML generator, use this JSON structure:
+
+```json
+{
+ "key": "propertyName",
+ "type": "string",
+ "caption": "Display Caption",
+ "description": "Optional description shown in Studio Pro",
+ "required": false,
+ "defaultValue": "optional default"
+}
+```
+
+---
+
+## Basic Types
+
+### string
+
+Simple text input.
+
+```json
+{
+ "key": "label",
+ "type": "string",
+ "caption": "Label",
+ "description": "Text label for the widget",
+ "defaultValue": "Click me"
+}
+```
+
+**XML Output:**
+
+```xml
+
+ Label
+ Text label for the widget
+
+```
+
+---
+
+### boolean
+
+True/false toggle.
+
+```json
+{
+ "key": "showIcon",
+ "type": "boolean",
+ "caption": "Show icon",
+ "description": "Display an icon next to the text",
+ "defaultValue": true
+}
+```
+
+**XML Output:**
+
+```xml
+
+ Show icon
+ Display an icon next to the text
+
+```
+
+---
+
+### integer
+
+Whole number input.
+
+```json
+{
+ "key": "maxItems",
+ "type": "integer",
+ "caption": "Maximum items",
+ "description": "Maximum number of items to display",
+ "defaultValue": 10
+}
+```
+
+**XML Output:**
+
+```xml
+
+ Maximum items
+ Maximum number of items to display
+
+```
+
+---
+
+### decimal
+
+Decimal number input.
+
+```json
+{
+ "key": "opacity",
+ "type": "decimal",
+ "caption": "Opacity",
+ "description": "Opacity level (0-1)",
+ "defaultValue": 0.8
+}
+```
+
+---
+
+## Text Types
+
+### textTemplate
+
+Text with parameter substitution. Allows dynamic text with placeholders.
+
+```json
+{
+ "key": "legend",
+ "type": "textTemplate",
+ "caption": "Legend",
+ "description": "Text template with parameters",
+ "required": false
+}
+```
+
+**XML Output:**
+
+```xml
+
+ Legend
+ Text template with parameters
+
+```
+
+---
+
+### expression
+
+Dynamic expression that can reference attributes and return computed values.
+
+```json
+{
+ "key": "visibleExpression",
+ "type": "expression",
+ "caption": "Visible",
+ "description": "Expression to determine visibility",
+ "defaultValue": "true"
+}
+```
+
+**With return type:**
+
+```json
+{
+ "key": "valueExpression",
+ "type": "expression",
+ "caption": "Value",
+ "returnType": "String"
+}
+```
+
+**XML Output (with returnType):**
+
+```xml
+
+ Value
+
+
+```
+
+---
+
+## Action Types
+
+### action
+
+Event handler that triggers actions (microflows, nanoflows, etc.).
+
+```json
+{
+ "key": "onClick",
+ "type": "action",
+ "caption": "On click",
+ "description": "Action to execute when clicked",
+ "required": false
+}
+```
+
+**XML Output:**
+
+```xml
+
+ On click
+ Action to execute when clicked
+
+```
+
+---
+
+## Data Types
+
+### attribute
+
+Links to an entity attribute. Must specify allowed attribute types.
+
+```json
+{
+ "key": "value",
+ "type": "attribute",
+ "caption": "Value",
+ "description": "Attribute to store the value",
+ "required": true,
+ "attributeTypes": ["String"]
+}
+```
+
+**Multiple attribute types:**
+
+```json
+{
+ "key": "numberValue",
+ "type": "attribute",
+ "attributeTypes": ["Integer", "Decimal", "Long"]
+}
+```
+
+**XML Output:**
+
+```xml
+
+ Value
+ Attribute to store the value
+
+
+
+
+```
+
+**Valid attributeTypes:**
+
+- `String`
+- `Integer`
+- `Long`
+- `Decimal`
+- `Boolean`
+- `DateTime`
+- `Enum`
+- `HashString`
+- `Binary`
+- `AutoNumber`
+
+---
+
+### datasource
+
+Data source for list-based widgets.
+
+```json
+{
+ "key": "dataSource",
+ "type": "datasource",
+ "caption": "Data source",
+ "description": "Source of items to display",
+ "isList": true,
+ "required": false
+}
+```
+
+**XML Output:**
+
+```xml
+
+ Data source
+ Source of items to display
+
+```
+
+---
+
+### association
+
+Links to an entity association.
+
+```json
+{
+ "key": "parent",
+ "type": "association",
+ "caption": "Parent association",
+ "required": false
+}
+```
+
+---
+
+### entity
+
+Entity selector.
+
+```json
+{
+ "key": "targetEntity",
+ "type": "entity",
+ "caption": "Target entity"
+}
+```
+
+---
+
+## Selection Types
+
+### enumeration
+
+Dropdown with predefined options. Must include `enumValues` array.
+
+```json
+{
+ "key": "alignment",
+ "type": "enumeration",
+ "caption": "Alignment",
+ "defaultValue": "left",
+ "enumValues": [
+ { "key": "left", "caption": "Left" },
+ { "key": "center", "caption": "Center" },
+ { "key": "right", "caption": "Right" }
+ ]
+}
+```
+
+**XML Output:**
+
+```xml
+
+ Alignment
+
+ Left
+ Center
+ Right
+
+
+```
+
+---
+
+### icon
+
+Icon picker.
+
+```json
+{
+ "key": "icon",
+ "type": "icon",
+ "caption": "Icon",
+ "required": false
+}
+```
+
+---
+
+### image
+
+Image picker.
+
+```json
+{
+ "key": "image",
+ "type": "image",
+ "caption": "Image",
+ "required": false
+}
+```
+
+---
+
+### file
+
+File selector.
+
+```json
+{
+ "key": "document",
+ "type": "file",
+ "caption": "Document"
+}
+```
+
+---
+
+## Container Types
+
+### widgets
+
+Container for child widgets. Used to create widget slots.
+
+```json
+{
+ "key": "content",
+ "type": "widgets",
+ "caption": "Content",
+ "description": "Widgets to display inside"
+}
+```
+
+**With datasource reference:**
+
+```json
+{
+ "key": "content",
+ "type": "widgets",
+ "caption": "Content",
+ "dataSource": "dataSource"
+}
+```
+
+**XML Output:**
+
+```xml
+
+ Content
+ Widgets to display inside
+
+```
+
+---
+
+### object
+
+Complex nested property with sub-properties. Used for repeating structures.
+
+```json
+{
+ "key": "columns",
+ "type": "object",
+ "caption": "Columns",
+ "isList": true,
+ "properties": [
+ {
+ "key": "header",
+ "type": "textTemplate",
+ "caption": "Header"
+ },
+ {
+ "key": "width",
+ "type": "integer",
+ "caption": "Width",
+ "defaultValue": 100
+ }
+ ]
+}
+```
+
+**XML Output:**
+
+```xml
+
+ Columns
+
+
+
+ Header
+
+
+ Width
+
+
+
+
+```
+
+---
+
+## System Properties
+
+System properties are predefined by Mendix. Reference them by key only.
+
+```json
+{
+ "systemProperties": ["Name", "TabIndex", "Visibility"]
+}
+```
+
+**Available system properties:**
+
+- `Name` - Widget name in Studio Pro
+- `TabIndex` - Tab order for accessibility
+- `Visibility` - Conditional visibility settings
+
+**XML Output:**
+
+```xml
+
+
+
+
+
+
+
+```
+
+---
+
+## Property Groups
+
+Properties can be organized into groups for better Studio Pro UI.
+
+```json
+{
+ "propertyGroups": [
+ {
+ "caption": "General",
+ "properties": ["label", "showIcon"]
+ },
+ {
+ "caption": "Events",
+ "properties": ["onClick", "onHover"]
+ }
+ ]
+}
+```
+
+---
+
+## Full Widget Definition Example
+
+```json
+{
+ "name": "TooltipButton",
+ "description": "A button with tooltip on hover",
+ "properties": [
+ {
+ "key": "buttonText",
+ "type": "textTemplate",
+ "caption": "Button text",
+ "description": "Text to display on the button"
+ },
+ {
+ "key": "tooltipText",
+ "type": "textTemplate",
+ "caption": "Tooltip text",
+ "description": "Text to show on hover"
+ },
+ {
+ "key": "buttonStyle",
+ "type": "enumeration",
+ "caption": "Style",
+ "defaultValue": "primary",
+ "enumValues": [
+ { "key": "primary", "caption": "Primary" },
+ { "key": "secondary", "caption": "Secondary" },
+ { "key": "danger", "caption": "Danger" }
+ ]
+ },
+ {
+ "key": "onClick",
+ "type": "action",
+ "caption": "On click",
+ "required": false
+ }
+ ],
+ "systemProperties": ["Name", "TabIndex", "Visibility"]
+}
+```
+
+---
+
+## Type Quick Reference
+
+| Type | Use Case | Requires |
+| -------------- | ---------------- | ----------------------- |
+| `string` | Simple text | - |
+| `boolean` | Toggle | `defaultValue` |
+| `integer` | Whole numbers | - |
+| `decimal` | Decimal numbers | - |
+| `textTemplate` | Dynamic text | - |
+| `expression` | Computed values | `returnType` (optional) |
+| `action` | Event handlers | - |
+| `attribute` | Entity binding | `attributeTypes` |
+| `datasource` | List data | `isList: true` |
+| `enumeration` | Dropdown | `enumValues` |
+| `widgets` | Child widgets | - |
+| `object` | Nested structure | `properties`, `isList` |
+| `icon` | Icon picker | - |
+| `image` | Image picker | - |
+| `association` | Entity relation | - |
diff --git a/packages/pluggable-widgets-mcp/docs/widget-patterns.md b/packages/pluggable-widgets-mcp/docs/widget-patterns.md
new file mode 100644
index 0000000000..6cf76aa6c4
--- /dev/null
+++ b/packages/pluggable-widgets-mcp/docs/widget-patterns.md
@@ -0,0 +1,556 @@
+# Mendix Widget Patterns
+
+This document provides reusable patterns for common widget types. Use these as templates when implementing widget components.
+
+---
+
+## Pattern: Display Widget
+
+Display widgets show read-only data. Examples: Badge, Progress Bar, Label.
+
+### Typical Properties
+
+```json
+{
+ "properties": [
+ { "key": "value", "type": "textTemplate", "caption": "Value" },
+ { "key": "type", "type": "enumeration", "caption": "Style", "enumValues": [...] },
+ { "key": "onClick", "type": "action", "caption": "On click", "required": false }
+ ],
+ "systemProperties": ["Name", "TabIndex", "Visibility"]
+}
+```
+
+### TSX Structure
+
+```tsx
+import { ReactNode, useCallback } from "react";
+import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
+import { MyWidgetContainerProps } from "../typings/MyWidgetProps";
+import "./ui/MyWidget.scss";
+
+export default function MyWidget(props: MyWidgetContainerProps): ReactNode {
+ const { value, type, onClick, tabIndex, class: className, style } = props;
+
+ const handleClick = useCallback(() => {
+ executeAction(onClick);
+ }, [onClick]);
+
+ const isClickable = onClick?.canExecute;
+
+ return (
+
+ {value?.value ?? ""}
+
+ );
+}
+```
+
+### SCSS Structure
+
+```scss
+.widget-mywidget {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: 4px;
+
+ &-primary {
+ background-color: var(--brand-primary);
+ color: white;
+ }
+
+ &-secondary {
+ background-color: var(--brand-secondary);
+ color: white;
+ }
+}
+```
+
+---
+
+## Pattern: Button Widget
+
+Button widgets trigger actions on click. May include icons, loading states.
+
+### Typical Properties
+
+```json
+{
+ "properties": [
+ { "key": "caption", "type": "textTemplate", "caption": "Caption" },
+ { "key": "icon", "type": "icon", "caption": "Icon", "required": false },
+ {
+ "key": "buttonStyle",
+ "type": "enumeration",
+ "caption": "Style",
+ "defaultValue": "primary",
+ "enumValues": [
+ { "key": "primary", "caption": "Primary" },
+ { "key": "secondary", "caption": "Secondary" },
+ { "key": "danger", "caption": "Danger" }
+ ]
+ },
+ { "key": "onClick", "type": "action", "caption": "On click" }
+ ],
+ "systemProperties": ["Name", "TabIndex", "Visibility"]
+}
+```
+
+### TSX Structure
+
+```tsx
+import { ReactNode, useCallback, KeyboardEvent } from "react";
+import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
+import { MyButtonContainerProps } from "../typings/MyButtonProps";
+import "./ui/MyButton.scss";
+
+export default function MyButton(props: MyButtonContainerProps): ReactNode {
+ const { caption, icon, buttonStyle, onClick, tabIndex, class: className, style } = props;
+
+ const handleClick = useCallback(() => {
+ executeAction(onClick);
+ }, [onClick]);
+
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ handleClick();
+ }
+ },
+ [handleClick]
+ );
+
+ const isDisabled = !onClick?.canExecute;
+
+ return (
+
+ );
+}
+```
+
+### SCSS Structure
+
+```scss
+.widget-mybutton {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ &-primary {
+ background-color: var(--brand-primary);
+ color: white;
+ }
+
+ &-secondary {
+ background-color: transparent;
+ border: 1px solid var(--brand-primary);
+ color: var(--brand-primary);
+ }
+
+ &-danger {
+ background-color: var(--brand-danger);
+ color: white;
+ }
+}
+```
+
+---
+
+## Pattern: Input Widget
+
+Input widgets bind to entity attributes for data entry.
+
+### Typical Properties
+
+```json
+{
+ "properties": [
+ {
+ "key": "value",
+ "type": "attribute",
+ "caption": "Value",
+ "attributeTypes": ["String"],
+ "required": true
+ },
+ { "key": "placeholder", "type": "textTemplate", "caption": "Placeholder", "required": false },
+ { "key": "readOnly", "type": "boolean", "caption": "Read-only", "defaultValue": false },
+ { "key": "onChange", "type": "action", "caption": "On change", "required": false },
+ { "key": "onEnter", "type": "action", "caption": "On enter", "required": false }
+ ],
+ "systemProperties": ["Name", "TabIndex", "Visibility"]
+}
+```
+
+### TSX Structure
+
+```tsx
+import { ReactNode, useCallback, ChangeEvent, KeyboardEvent } from "react";
+import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
+import { MyInputContainerProps } from "../typings/MyInputProps";
+import "./ui/MyInput.scss";
+
+export default function MyInput(props: MyInputContainerProps): ReactNode {
+ const { value, placeholder, readOnly, onChange, onEnter, tabIndex, class: className, style } = props;
+
+ const handleChange = useCallback(
+ (event: ChangeEvent) => {
+ if (value?.status === "available" && !value.readOnly) {
+ value.setValue(event.target.value);
+ executeAction(onChange);
+ }
+ },
+ [value, onChange]
+ );
+
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.key === "Enter") {
+ executeAction(onEnter);
+ }
+ },
+ [onEnter]
+ );
+
+ const isReadOnly = readOnly || value?.readOnly;
+
+ return (
+
+ );
+}
+```
+
+### SCSS Structure
+
+```scss
+.widget-myinput {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ font-size: 14px;
+
+ &:focus {
+ outline: none;
+ border-color: var(--brand-primary);
+ box-shadow: 0 0 0 2px rgba(var(--brand-primary-rgb), 0.2);
+ }
+
+ &:read-only {
+ background-color: var(--bg-color-secondary);
+ }
+}
+```
+
+---
+
+## Pattern: Container Widget
+
+Container widgets hold child widgets. Examples: Fieldset, Card, Accordion.
+
+### Typical Properties
+
+```json
+{
+ "properties": [
+ { "key": "content", "type": "widgets", "caption": "Content" },
+ { "key": "header", "type": "textTemplate", "caption": "Header", "required": false },
+ { "key": "collapsible", "type": "boolean", "caption": "Collapsible", "defaultValue": false }
+ ],
+ "systemProperties": ["Name", "TabIndex", "Visibility"]
+}
+```
+
+### TSX Structure
+
+```tsx
+import { ReactNode, useState, useCallback } from "react";
+import { MyContainerContainerProps } from "../typings/MyContainerProps";
+import "./ui/MyContainer.scss";
+
+export default function MyContainer(props: MyContainerContainerProps): ReactNode {
+ const { content, header, collapsible, tabIndex, class: className, style } = props;
+ const [isOpen, setIsOpen] = useState(true);
+
+ const handleToggle = useCallback(() => {
+ if (collapsible) {
+ setIsOpen(prev => !prev);
+ }
+ }, [collapsible]);
+
+ return (
+
+ {header?.value && (
+
+ {header.value}
+ {collapsible && ▼}
+
+ )}
+ {isOpen &&
{content}
}
+
+ );
+}
+```
+
+### SCSS Structure
+
+```scss
+.widget-mycontainer {
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ overflow: hidden;
+
+ &-header {
+ padding: 12px 16px;
+ background-color: var(--bg-color-secondary);
+ font-weight: 600;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ &[role="button"] {
+ cursor: pointer;
+ }
+ }
+
+ &-toggle {
+ transition: transform 0.2s;
+
+ &.open {
+ transform: rotate(180deg);
+ }
+ }
+
+ &-content {
+ padding: 16px;
+ }
+}
+```
+
+---
+
+## Pattern: Data List Widget
+
+Data list widgets display items from a datasource.
+
+### Typical Properties
+
+```json
+{
+ "properties": [
+ { "key": "dataSource", "type": "datasource", "caption": "Data source", "isList": true },
+ { "key": "content", "type": "widgets", "caption": "Content", "dataSource": "dataSource" },
+ { "key": "emptyMessage", "type": "textTemplate", "caption": "Empty message", "required": false },
+ { "key": "onItemClick", "type": "action", "caption": "On item click", "required": false }
+ ],
+ "systemProperties": ["Name", "Visibility"]
+}
+```
+
+### TSX Structure
+
+```tsx
+import { ReactNode, useCallback } from "react";
+import { ValueStatus, ObjectItem } from "mendix";
+import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
+import { MyListContainerProps } from "../typings/MyListProps";
+import "./ui/MyList.scss";
+
+export default function MyList(props: MyListContainerProps): ReactNode {
+ const { dataSource, content, emptyMessage, onItemClick, class: className, style } = props;
+
+ // Loading state
+ if (dataSource?.status !== ValueStatus.Available) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ const items = dataSource?.items ?? [];
+
+ // Empty state
+ if (items.length === 0) {
+ return (
+
+ {emptyMessage?.value ?? "No items"}
+
+ );
+ }
+
+ return (
+
+ {items.map((item: ObjectItem) => (
+
executeAction(onItemClick)}>
+ {content?.get(item)}
+
+ ))}
+
+ );
+}
+```
+
+### SCSS Structure
+
+```scss
+.widget-mylist {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ &-loading,
+ &-empty {
+ padding: 16px;
+ text-align: center;
+ color: var(--text-color-secondary);
+ }
+
+ &-item {
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+
+ &:hover {
+ background-color: var(--bg-color-hover);
+ }
+ }
+}
+```
+
+---
+
+## Common Imports
+
+Every widget typically needs these imports:
+
+```tsx
+// React
+import { ReactNode, useCallback, useState } from "react";
+
+// Mendix helpers
+import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
+import { ValueStatus } from "mendix";
+
+// Generated types (from XML via pwt build)
+import { MyWidgetContainerProps } from "../typings/MyWidgetProps";
+
+// Styles
+import "./ui/MyWidget.scss";
+```
+
+---
+
+## Key Patterns
+
+### Action Execution
+
+Always use `executeAction` from the platform helpers:
+
+```tsx
+import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
+
+const handleClick = useCallback(() => {
+ executeAction(props.onClick);
+}, [props.onClick]);
+
+// Check if action can execute
+const isClickable = props.onClick?.canExecute;
+```
+
+### Attribute Value Handling
+
+Check status before reading/writing:
+
+```tsx
+// Reading
+const displayValue = props.value?.value ?? "";
+
+// Writing
+if (props.value?.status === "available" && !props.value.readOnly) {
+ props.value.setValue(newValue);
+}
+```
+
+### Loading States
+
+Handle datasource loading:
+
+```tsx
+if (props.dataSource?.status !== ValueStatus.Available) {
+ return Loading...
;
+}
+```
+
+### Accessibility
+
+Always include proper accessibility attributes:
+
+```tsx
+