index.tsā¢7.21 kB
#!/usr/bin/env node
/**
* OneTech MCP Server
* Extract and document Mendix Studio Pro modules via Model Context Protocol
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { execFile } from "child_process";
import { promisify } from "util";
import { readFile, access, mkdir } from "fs/promises";
import { resolve, join, dirname } from "path";
import { fileURLToPath } from "url";
import { constants } from "fs";
const execFileAsync = promisify(execFile);
// Get __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Server configuration
*/
const SERVER_CONFIG = {
name: "onetech-mcp-server",
version: "0.1.0",
description: "OneTech MCP Server - Extract and document Mendix Studio Pro modules",
};
/**
* Default mx.exe path (Studio Pro 11.3.0)
*/
const DEFAULT_MX_PATH = "D:\\Program Files\\Mendix\\11.3.0.80734\\modeler\\mx.exe";
/**
* Validates that a file exists and is accessible
*/
async function validateFileExists(filePath: string, description: string): Promise<void> {
try {
await access(filePath, constants.R_OK);
} catch (error) {
throw new Error(`${description} not found or not accessible: ${filePath}`);
}
}
/**
* Validates that mx.exe exists at the specified path
*/
async function validateMxExe(mxPath: string): Promise<void> {
await validateFileExists(mxPath, "mx.exe");
}
/**
* Validates that the .mpr file exists
*/
async function validateMprFile(mprPath: string): Promise<void> {
await validateFileExists(mprPath, "MPR file");
}
/**
* Ensures output directory exists
*/
async function ensureOutputDir(outputPath: string): Promise<void> {
try {
await mkdir(outputPath, { recursive: true });
} catch (error) {
throw new Error(`Failed to create output directory: ${outputPath}`);
}
}
/**
* Runs mx.exe command and returns result
*/
async function runMxCommand(mxPath: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
try {
const result = await execFileAsync(mxPath, args, {
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large JSON outputs
timeout: 300000, // 5 minute timeout
});
return result;
} catch (error: any) {
throw new Error(`mx.exe execution failed: ${error.message}`);
}
}
/**
* Extracts module documentation using mx.exe dump-mpr commands
*/
async function extractModule(
mprPath: string,
moduleName: string,
outputPath: string,
mxPath: string = DEFAULT_MX_PATH
): Promise<{
success: boolean;
files: Array<{ name: string; size: number }>;
message: string;
}> {
// Validate inputs
await validateMxExe(mxPath);
await validateMprFile(mprPath);
await ensureOutputDir(outputPath);
const files: Array<{ name: string; size: number }> = [];
const errors: string[] = [];
// Define the 4 mx.exe commands to extract module data
const commands = [
{
name: "DomainModel",
unitType: "DomainModels$DomainModel",
outputFile: join(outputPath, `${moduleName}-DomainModel.json`),
},
{
name: "Pages",
unitType: "Pages$Page",
outputFile: join(outputPath, `${moduleName}-Pages.json`),
},
{
name: "Microflows",
unitType: "Microflows$Microflow",
outputFile: join(outputPath, `${moduleName}-Microflows.json`),
},
{
name: "Enumerations",
unitType: "Enumerations$Enumeration",
outputFile: join(outputPath, `${moduleName}-Enumerations.json`),
},
];
// Execute each command sequentially
for (const cmd of commands) {
try {
const args = ["dump-mpr", "--unit-type", cmd.unitType, "--module-names", moduleName, "--output-file", cmd.outputFile, mprPath];
await runMxCommand(mxPath, args);
// Verify file was created and get size
let fileContent = await readFile(cmd.outputFile, "utf-8");
// Remove UTF-8 BOM if present (mx.exe outputs BOM)
if (fileContent.charCodeAt(0) === 0xfeff) {
fileContent = fileContent.substring(1);
}
files.push({
name: `${cmd.name}.json`,
size: Buffer.byteLength(fileContent, "utf-8"),
});
} catch (error: any) {
errors.push(`${cmd.name}: ${error.message}`);
}
}
// Return results
if (files.length === 0) {
return {
success: false,
files: [],
message: `Failed to extract module. Errors: ${errors.join(", ")}`,
};
}
if (errors.length > 0) {
return {
success: true,
files,
message: `Extracted ${files.length}/4 files successfully. Partial errors: ${errors.join(", ")}`,
};
}
return {
success: true,
files,
message: `Successfully extracted ${files.length} files from module '${moduleName}'`,
};
}
/**
* Main server initialization
*/
async function main() {
const server = new Server(
{
name: SERVER_CONFIG.name,
version: SERVER_CONFIG.version,
},
{
capabilities: {
tools: {},
},
}
);
/**
* Handler: List available tools
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "onetech_extract_module",
description: "Extract domain model, pages, microflows, and enumerations from a Mendix module using mx.exe. Returns JSON files with complete module structure.",
inputSchema: {
type: "object",
properties: {
mprPath: {
type: "string",
description: "Absolute path to the .mpr file (e.g., D:\\Projects\\OneTech.mpr)",
},
moduleName: {
type: "string",
description: "Name of the module to extract (e.g., RequestHub)",
},
outputPath: {
type: "string",
description: "Absolute path to output directory for JSON files",
},
mxPath: {
type: "string",
description: `Optional: Path to mx.exe (default: ${DEFAULT_MX_PATH})`,
},
},
required: ["mprPath", "moduleName", "outputPath"],
},
},
],
};
});
/**
* Handler: Execute tool
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "onetech_extract_module") {
const { mprPath, moduleName, outputPath, mxPath } = request.params.arguments as {
mprPath: string;
moduleName: string;
outputPath: string;
mxPath?: string;
};
try {
const result = await extractModule(mprPath, moduleName, outputPath, mxPath || DEFAULT_MX_PATH);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: false,
files: [],
message: `Error: ${error.message}`,
},
null,
2
),
},
],
isError: true,
};
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("OneTech MCP Server running on stdio");
}
// Run server
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});