Skip to content

Commit 0ff0a28

Browse files
committed
feat(mendix-widgets-mcp): dockerize the server
1 parent c6b185d commit 0ff0a28

25 files changed

+1340
-975
lines changed

automation/mendix-widgets-mcp/src/index.ts

Lines changed: 0 additions & 794 deletions
This file was deleted.
File renamed without changes.

mcp/Dockerfile

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Multi-stage build for mendix-pluggable-mcp
2+
FROM node:22-alpine AS base
3+
4+
# Install pnpm
5+
RUN corepack enable && corepack prepare pnpm@10.17.0 --activate
6+
7+
# Set working directory
8+
WORKDIR /app
9+
10+
# Stage 1: Install dependencies for the entire monorepo
11+
FROM base AS deps
12+
13+
# Copy root package files
14+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
15+
16+
# Copy pnpm configuration files (required for lockfile validation)
17+
COPY .pnpmfile.cjs .npmrc ./
18+
19+
# Copy patches directory (required by pnpm)
20+
COPY patches ./patches/
21+
22+
# Copy automation-utils dependency (complete structure)
23+
COPY automation/utils ./automation/utils/
24+
25+
# Copy MCP server package files
26+
COPY mcp/package.json mcp/
27+
28+
# Install all dependencies (ignore scripts to skip postinstall)
29+
RUN pnpm install --frozen-lockfile --filter @mendix/mendix-pluggable-mcp... --ignore-scripts
30+
31+
# Stage 2: Build the MCP server
32+
FROM deps AS builder
33+
34+
# Copy entire automation directory
35+
COPY automation ./automation/
36+
37+
# Copy docs and packages directories (needed by MCP server at runtime)
38+
COPY docs ./docs/
39+
COPY packages ./packages/
40+
41+
# Build the MCP server
42+
WORKDIR /app/mcp
43+
RUN pnpm build
44+
45+
# Stage 3: Production image
46+
FROM base AS runner
47+
48+
# Copy root configuration files
49+
COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml /app/
50+
COPY --from=builder /app/.pnpmfile.cjs /app/.npmrc /app/
51+
52+
# Copy monorepo directories that MCP server needs to access
53+
COPY --from=builder /app/patches /app/patches/
54+
COPY --from=builder /app/automation /app/automation/
55+
COPY --from=builder /app/docs /app/docs/
56+
COPY --from=builder /app/packages /app/packages/
57+
58+
# Install only production dependencies (ignore scripts to skip postinstall)
59+
WORKDIR /app
60+
RUN pnpm install --frozen-lockfile --filter @mendix/mendix-pluggable-mcp... --prod --ignore-scripts
61+
62+
# Environment variables
63+
ENV NODE_ENV=production
64+
ENV MX_PROJECT_PATH=""
65+
ENV REPO_ROOT=/app
66+
67+
# Set working directory to app root
68+
# When running with volume mounts, REPO_ROOT will be overridden to /workspace
69+
WORKDIR /app
70+
71+
# Run the MCP server
72+
# The server binary is at /app/mcp/build/index.js
73+
# It will access repository files from REPO_ROOT (either /app or /workspace)
74+
CMD ["node", "/app/mcp/build/index.js"]

automation/mendix-widgets-mcp/README.md renamed to mcp/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Mendix Widgets Copilot MCP Server
1+
# Mendix Widgets MCP Server
22

33
An MCP (Model Context Protocol) server that provides AI-powered tooling for the `web-widgets` monorepo. Enables discovery, inspection, building, testing, property manipulation, and verification of Mendix widget packages through natural language interactions.
44

@@ -119,7 +119,7 @@ The MCP server is **pre-configured** for the team! Just follow these steps:
119119
pnpm run build:mcp
120120

121121
# Or from the MCP directory:
122-
cd automation/mendix-widgets-copilot && pnpm build
122+
cd mcp && pnpm build
123123
```
124124

125125
2. **Restart Cursor** - The MCP server will be automatically available via the workspace configuration in `.cursor/mcp.json`
@@ -138,9 +138,9 @@ The workspace already includes the MCP configuration in `.cursor/mcp.json`:
138138
```json
139139
{
140140
"mcpServers": {
141-
"mendix-widgets-copilot": {
141+
"mendix-pluggable-mcp": {
142142
"command": "node",
143-
"args": ["automation/mendix-widgets-copilot/build/index.js"],
143+
"args": ["mcp/build/index.js"],
144144
"env": {
145145
"MX_PROJECT_PATH": "${workspaceFolder}/../test-project"
146146
},
@@ -155,7 +155,7 @@ The workspace already includes the MCP configuration in `.cursor/mcp.json`:
155155
For hot-reload during MCP server development:
156156

157157
```bash
158-
cd web-widgets/automation/mendix-widgets-copilot
158+
cd web-widgets/mcp
159159
pnpm dev
160160
```
161161

mcp/docker-build.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
7+
8+
IMAGE_NAME="mendix-pluggable-mcp"
9+
IMAGE_TAG="latest"
10+
11+
echo "Building Docker image for Mendix Widgets MCP Server..."
12+
echo "Repository root: ${REPO_ROOT}"
13+
14+
# Clean up any orphaned containers
15+
echo "Cleaning up orphaned containers..."
16+
docker ps -a --filter "ancestor=${IMAGE_NAME}:${IMAGE_TAG}" -q | xargs -r docker rm -f 2>/dev/null || true
17+
18+
cd "${REPO_ROOT}"
19+
20+
# Build the Docker image
21+
docker build \
22+
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
23+
-f mcp/Dockerfile \
24+
.
25+
26+
echo ""
27+
echo "Docker image built successfully!"
28+
echo "Image: ${IMAGE_NAME}:${IMAGE_TAG}"
29+
echo ""
30+
echo "To run the container:"
31+
echo " docker run -i ${IMAGE_NAME}:${IMAGE_TAG}"
32+
echo ""
33+
echo "To run with volume mount for workspace:"
34+
echo " docker run -i -v \"\$(pwd):/workspace\" -e CWD=/workspace ${IMAGE_NAME}:${IMAGE_TAG}"

automation/mendix-widgets-mcp/package.json renamed to mcp/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "@mendix/mendix-widgets-copilot",
2+
"name": "@mendix/mendix-pluggable-mcp",
33
"version": "0.1.0",
4-
"description": "Mendix Widgets Copilot MCP server for the web-widgets monorepo",
4+
"description": "Mendix Widgets MCP server for the web-widgets monorepo",
55
"license": "Apache-2.0",
66
"private": true,
77
"type": "module",
@@ -17,7 +17,6 @@
1717
"start": "node build/index.js"
1818
},
1919
"dependencies": {
20-
"@mendix/automation-utils": "workspace:^",
2120
"@modelcontextprotocol/sdk": "^1.17.4",
2221
"fast-xml-parser": "^4.5.3",
2322
"zod": "^3.25.67"

automation/mendix-widgets-mcp/rollup.config.mjs renamed to mcp/rollup.config.mjs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ export default {
1818
"path",
1919
"url",
2020
"child_process",
21-
"util",
22-
// Keep workspace dependencies external (they won't be bundled)
23-
"@mendix/automation-utils"
21+
"util"
2422
],
2523
plugins: [
2624
// Handle JSON imports
File renamed without changes.

automation/mendix-widgets-mcp/src/guardrails.ts renamed to mcp/src/guardrails.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,18 @@ export class Guardrails {
4343
const resolvedPath = resolve(targetPath);
4444
const relativePath = relative(this.repoRoot, resolvedPath);
4545

46+
// Debug logging
47+
console.error(`[Guardrails] validatePath called with: ${targetPath}`);
48+
console.error(`[Guardrails] repoRoot: ${this.repoRoot}`);
49+
console.error(`[Guardrails] resolvedPath: ${resolvedPath}`);
50+
console.error(`[Guardrails] relativePath: ${relativePath}`);
51+
4652
// Must be within repo
4753
if (relativePath.startsWith("..")) {
48-
throw new GuardrailError(`Path ${targetPath} is outside the repository`, "PATH_OUTSIDE_REPO");
54+
throw new GuardrailError(
55+
`Path ${targetPath} is outside the repository. RepoRoot: ${this.repoRoot}, Resolved: ${resolvedPath}, Relative: ${relativePath}`,
56+
"PATH_OUTSIDE_REPO"
57+
);
4958
}
5059

5160
// Check against blocked paths
@@ -111,7 +120,11 @@ export class Guardrails {
111120
* Validate that a package path is a valid widget/module
112121
*/
113122
async validatePackage(packagePath: string): Promise<string> {
114-
const validatedPath = await this.validatePath(packagePath);
123+
// First resolve the path - it might be relative or absolute
124+
const resolvedPath = resolve(packagePath);
125+
126+
// Validate it's accessible
127+
const validatedPath = await this.validatePath(resolvedPath);
115128

116129
// Must have package.json
117130
const packageJsonPath = join(validatedPath, "package.json");
@@ -127,7 +140,7 @@ export class Guardrails {
127140

128141
if (pathParts.length < 3 || pathParts[0] !== "packages") {
129142
throw new GuardrailError(
130-
`${packagePath} is not in expected packages directory structure`,
143+
`${packagePath} is not in expected packages directory structure. Path: ${relativePath}, Parts: ${pathParts.join("/")}`,
131144
"INVALID_PACKAGE_STRUCTURE"
132145
);
133146
}

automation/mendix-widgets-mcp/src/helpers.ts renamed to mcp/src/helpers.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@ export async function resolveWidgetFiles(packagePath: string): Promise<{
7373
return { widgetName, srcPath, widgetXmlPath, editorConfigPath, editorPreviewPath };
7474
}
7575

76+
/**
77+
* Find a widget package by name (e.g., "combobox-web" or "@mendix/combobox-web")
78+
* Returns the full path to the package directory
79+
*/
80+
export async function findWidgetByName(packagesDir: string, widgetName: string): Promise<string | null> {
81+
const packages = await scanPackages(packagesDir);
82+
83+
// Normalize the widget name
84+
const normalizedName = widgetName.startsWith("@mendix/") ? widgetName : `@mendix/${widgetName}`;
85+
86+
// Try exact match first
87+
let found = packages.find(pkg => pkg.name === normalizedName);
88+
89+
// Try partial match (e.g., "combobox" matches "combobox-web")
90+
if (!found) {
91+
found = packages.find(
92+
pkg => pkg.name.includes(widgetName.replace("@mendix/", "")) || pkg.path.includes(widgetName)
93+
);
94+
}
95+
96+
return found ? found.path : null;
97+
}
98+
7699
export async function scanPackages(packagesDir: string): Promise<PackageInfo[]> {
77100
const packages: PackageInfo[] = [];
78101

0 commit comments

Comments
 (0)