morphir-elm
Version:
Elm bindings for Morphir
262 lines (255 loc) • 11.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const zod_1 = require("zod");
const commander_1 = require("commander"); // Import commander
const path = __importStar(require("path"));
// Read the package.json of this package
const packageJson = require(path.join(__dirname, "../../package.json"));
// Process arguments using commander
const program = new commander_1.Command();
program
.option("--elm-command <command>", "Specify the Elm command")
.option("--root-dir <directory>", "Specify the root directory of the Morphir project")
.parse(process.argv);
const options = program.opts();
const elmCommand = options.elmCommand;
const rootDir = options.rootDir; // Capture the new option
if (!rootDir) {
throw new Error("Root directory is not specified. Use the --root-dir option.");
}
// Create an MCP server
const server = new mcp_js_1.McpServer({
name: "Morphir MCP Server",
version: packageJson.version // Use the version from package.json
}, {
// Define the server's resources
instructions: `
This is a Morphir MCP server. It provides tools to interact with Morphir projects.
The server supports the following tools:
- addModule: Adds a module to the Morphir project. A module can contain types and
functions defined using the syntax of the Elm programming language.
`
});
// Utility function to ensure morphir.json exists, creates it if missing
async function ensureMorphirJson(rootDir) {
const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
const path = await Promise.resolve().then(() => __importStar(require("path")));
const morphirJsonPath = path.join(rootDir, "morphir.json");
let existed = true;
try {
await fs.access(morphirJsonPath);
}
catch {
existed = false;
const morphirJsonContent = JSON.stringify({
name: "MorphirMCP",
sourceDirectory: "src"
}, null, 4);
await fs.writeFile(morphirJsonPath, morphirJsonContent, "utf8");
}
return { existed };
}
// Utility function to ensure elm.json exists, creates it if missing
async function ensureElmJson(rootDir, sourceDirectory) {
const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
const path = await Promise.resolve().then(() => __importStar(require("path")));
const elmJsonPath = path.join(rootDir, "elm.json");
let existed = true;
try {
await fs.access(elmJsonPath);
}
catch {
existed = false;
const elmJsonContent = JSON.stringify({
type: "application",
"source-directories": [sourceDirectory],
"elm-version": "0.19.1",
dependencies: {
direct: {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0"
},
indirect: {
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.3"
}
},
"test-dependencies": {
direct: {},
indirect: {}
}
}, null, 4);
await fs.writeFile(elmJsonPath, elmJsonContent, "utf8");
}
return { existed };
}
// Add a new tool to add a module to the Morphir project
server.tool("addModule", `This tool adds a module to the Morphir project. A module can contain types and
functions defined using the syntax of the Elm programming language.
The tool takes two arguments: the name of the module and the content of the module.
The name of the module should be a valid Elm module name, preferably single word,
and the content should be a valid Elm code without the module declaration line.
Imports are supported, but should be limited to the elm/core library.
Every module name will implicitly be prefixed with "MorphirMCP.".
Follow these rules when implementing the Elm logic:
- Exclude the module declaration.
- Only use imports from elm/core library.
- When defining custom types prefer to use positional argument over record structures as arguments.
- Implement only what the user asked for, do not add extra utility or testing functions.
`, {
moduleName: zod_1.z.string(),
content: zod_1.z.string()
}, async ({ moduleName, content }) => {
if (!rootDir) {
throw new Error("Root directory is not specified. Use the --root-dir option.");
}
const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
const path = await Promise.resolve().then(() => __importStar(require("path")));
const { exec } = await Promise.resolve().then(() => __importStar(require("child_process")));
const { promisify } = await Promise.resolve().then(() => __importStar(require("util")));
const execAsync = promisify(exec);
// Ensure morphir.json exists in the root directory, create if missing
await ensureMorphirJson(rootDir);
// Read morphir.json to get the sourceDirectory and name
const morphirConfigPath = path.join(rootDir, "morphir.json");
const morphirConfig = JSON.parse(await fs.readFile(morphirConfigPath, "utf8"));
const sourceDirectory = morphirConfig.sourceDirectory;
const projectName = morphirConfig.name;
if (!sourceDirectory) {
throw new Error("sourceDirectory is not defined in morphir.json.");
}
if (!projectName) {
throw new Error("name is not defined in morphir.json.");
}
// Prepend the module declaration to the content
const moduleDeclaration = `module ${projectName}.${moduleName} exposing (..)\n\n`;
const fullContent = moduleDeclaration + content;
// Construct the full path for the module
const modulePath = path.join(rootDir, sourceDirectory, projectName, `${moduleName}.elm`);
// Ensure the directory exists
await fs.mkdir(path.dirname(modulePath), { recursive: true });
// Write the module file
await fs.writeFile(modulePath, fullContent, "utf8");
// Ensure elm.json exists in the root directory, create if missing
const { existed: elmJsonExisted } = await ensureElmJson(rootDir, sourceDirectory);
// Run "elm make" on the saved module file
try {
await execAsync(`${elmCommand} make ${modulePath}`, { cwd: rootDir });
}
catch (error) {
await fs.unlink(modulePath).catch(() => { });
if (!elmJsonExisted) {
const elmJsonPath = path.join(rootDir, "elm.json");
await fs.unlink(elmJsonPath).catch(() => { });
}
return {
content: [{ type: "text", text: `Elm compile error:\n${error.stderr || error.message}` }]
};
}
// Run "morphir make" in the root directory
try {
const { stdout } = await execAsync("morphir make", { cwd: rootDir });
return {
content: [{ type: "text", text: `Module ${moduleName} added successfully.\n${stdout}` }]
};
}
catch (error) {
await fs.unlink(modulePath).catch(() => { });
if (!elmJsonExisted) {
const elmJsonPath = path.join(rootDir, "elm.json");
await fs.unlink(elmJsonPath).catch(() => { });
}
return {
content: [{ type: "text", text: `Failed to run "morphir make":\n${error.stderr || error.message}` }]
};
}
});
// Add a new tool to set test cases for a module and function
server.tool("setTestCases", `This tool sets test cases for a given module and function in the Morphir project.
It takes a module name, a function name, and a JSON object representing the test cases.
Each test case should have the following structure:
{
"inputs": [value1, value2, ...], // Array of input values (can be null for optional inputs)
"expectedOutput": value, // The expected output value
"description": "string" // Description of the test case
}
Where value can be any valid Elm value, including null for optional inputs. Use the following rules to turn values into JSON:
- Constructors are represented as arrays where the first item is the name of the constructor as a string and the remaining items are the arguments.
- Zero argument constructors are represented as a single element array with the name of the constructor. (e.g., ["Just"] for Maybe Just constructor)
`, {
moduleName: zod_1.z.string(),
functionName: zod_1.z.string(),
testCases: zod_1.z.array(zod_1.z.object({
inputs: zod_1.z.array(zod_1.z.any()),
expectedOutput: zod_1.z.any(),
description: zod_1.z.string()
}))
}, async ({ moduleName, functionName, testCases }) => {
const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
const path = await Promise.resolve().then(() => __importStar(require("path")));
const testsPath = path.join(rootDir, "morphir-tests.json");
let testSuite = [];
// Check if morphir-tests.json exists, if not create it as an empty object
try {
const content = await fs.readFile(testsPath, "utf8");
testSuite = JSON.parse(content);
}
catch {
testSuite = [];
}
const fqName = [[["morphir", "m", "c", "p"]], [stringToName(moduleName)], stringToName(functionName)];
// Update or add the test cases for this function
testSuite.push([fqName, testCases]);
// Write the updated test suite back to the file
await fs.writeFile(testsPath, JSON.stringify(testSuite, null, 4), "utf8");
return {
content: [{
type: "text",
text: `Test cases for ${fqName} have been added successfully.`
}]
};
});
// Start receiving messages on stdin and sending messages on stdout
const transport = new stdio_js_1.StdioServerTransport();
// Try to start the server
server.connect(transport).then(() => {
// nothing to log here since stdout is used for MCP transport
}).catch((error) => {
console.error("Failed to connect server:", error);
});
// Convert a camel-case string to an array of lowercase words (name)
function stringToName(str) {
return str
.replace(/([a-z])([A-Z])/g, '$1 $2') // Add space before uppercase letters
.split(' ') // Split by spaces
.map(word => word.toLowerCase()); // Convert to lowercase
}