Skip to main content
Glama

Isolator MCP Server

by Ompragash
index.ts15.9 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, TextContent, CallToolResult, } from "@modelcontextprotocol/sdk/types.js"; import fs from "fs/promises"; import path from "path"; import os from "os"; import { spawn } from "child_process"; import { z } from "zod"; import { fileURLToPath } from 'url'; // Define structure for config file interface LanguageSetting { image: string; } interface ServerConfig { isolatorPath: string; defaultTimeoutSeconds: number; defaultMemoryMB: number; defaultCpus: number; defaultPidsLimit: number; defaultNetworkMode: string; defaultCapDrop: string[]; defaultReadOnlyRootfs: boolean; defaultContainerWorkdir: string; languageSettings: { [key: string]: LanguageSetting; }; promptsDir: string; // Renamed from promptsDir if needed, reflects config key } // Define structure for Go CLI JSON output interface ExecutionResult { status: "success" | "timeout" | "error"; exitCode: number; stdout: string; stderr: string; durationMs: number; error?: string; imageTag?: string; } // --- Configuration Loading --- let serverConfig: ServerConfig; async function loadConfig(): Promise<ServerConfig> { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const configPath = path.resolve(__dirname, '..', 'isolator_config.json'); // Adjust path relative to build output try { const configFile = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configFile) as ServerConfig; // Basic validation if (!config.isolatorPath || !config.languageSettings || !config.promptsDir) { // Check promptsDir too throw new Error("Invalid config: missing isolatorPath, languageSettings, or promptsDir"); } console.error(`Configuration loaded from ${configPath}`); return config; } catch (error) { console.error(`Error loading config from ${configPath}:`, error); throw new Error(`Failed to load configuration: ${error instanceof Error ? error.message : String(error)}`); } } // --- Tool Definition --- const additionalFileSchema = z.object({ filename: z.string().describe("Name of the file (including extension)"), content: z.string().describe("Content of the file"), }); // Input schema allowing either direct code OR a snippet name const executeCodeInputSchema = z.object({ language: z.string().optional().describe("The programming language (python, go, javascript). Required unless using snippet_name."), entrypoint_code: z.string().optional().describe("The main code content to execute. Required unless using snippet_name."), entrypoint_filename: z.string().optional().describe("Optional filename for the main code (defaults based on language)"), additional_files: z.array(additionalFileSchema).optional().describe("Optional array of additional files needed for execution"), snippet_name: z.string().optional().describe("Name of a pre-defined code snippet to execute (e.g., 'hello_world'). Mutually exclusive with entrypoint_code/language.") }).refine(data => (data.snippet_name && !data.entrypoint_code && !data.language) || (!data.snippet_name && data.entrypoint_code && data.language), { message: "Either 'snippet_name' or BOTH 'entrypoint_code' and 'language' must be provided, but not both sets.", }); // --- Server Setup --- const server = new Server( { name: "isolator-mcp", version: "0.1.0", }, { capabilities: { tools: {}, // Indicates server provides tools }, } ); // --- Tool Handlers --- server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "execute_code", description: "Executes code (Python, Go, JavaScript) in a secure, isolated container environment.", inputSchema: { type: "object", properties: { language: { type: "string", description: "The programming language (python, go, javascript). Required unless using snippet_name." }, entrypoint_code: { type: "string", description: "The main code content to execute. Required unless using snippet_name." }, entrypoint_filename: { type: "string", description: "Optional filename for the main code (defaults based on language)." }, additional_files: { type: "array", description: "Optional array of additional files needed for execution", items: { type: "object", properties: { filename: { type: "string", description: "Name of the file (including extension)" }, content: { type: "string", description: "Content of the file" }, }, required: ["filename", "content"], }, }, snippet_name: { type: "string", description: "Name of a pre-defined code snippet to execute (e.g., 'hello_world'). Mutually exclusive with entrypoint_code/language.", }, }, // Rely on Zod validation in the handler for mutual exclusivity. }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => { if (request.params.name !== "execute_code") { throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } // Validate input using Zod schema const parseResult = executeCodeInputSchema.safeParse(request.params.arguments); if (!parseResult.success) { console.error("Invalid input:", parseResult.error.errors); throw new McpError(ErrorCode.InvalidParams, `Invalid arguments: ${parseResult.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); } let languageInput: string; let codeInput: string; let languageDetectedFromSnippet = false; const { entrypoint_filename, additional_files, snippet_name } = parseResult.data; let finalEntrypointFilename: string; // --- Determine code input and language: Snippet or Direct --- if (snippet_name) { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const snippetsDir = path.resolve(__dirname, '..', serverConfig.promptsDir); // Use config value const snippetExt = path.extname(snippet_name); const baseSnippetName = snippetExt ? path.basename(snippet_name, snippetExt) : snippet_name; let foundSnippetPath: string | undefined; let detectedLang: string | undefined; let snippetFilename: string | undefined; const possibleExtensions: { [key: string]: string } = { ".py": "python", ".go": "go", ".js": "javascript", ".mjs": "javascript", ".cjs": "javascript", }; if (snippetExt && possibleExtensions[snippetExt]) { detectedLang = possibleExtensions[snippetExt]; foundSnippetPath = path.join(snippetsDir, `${baseSnippetName}${snippetExt}`); snippetFilename = `${baseSnippetName}${snippetExt}`; } else { for (const [ext, lang] of Object.entries(possibleExtensions)) { const testPath = path.join(snippetsDir, `${baseSnippetName}${ext}`); try { await fs.access(testPath); foundSnippetPath = testPath; detectedLang = lang; snippetFilename = `${baseSnippetName}${ext}`; break; } catch { /* continue */ } } } if (!foundSnippetPath || !detectedLang) { throw new McpError(ErrorCode.InvalidParams, `Snippet '${snippet_name}' not found in '${snippetsDir}' or language could not be determined.`); } try { codeInput = await fs.readFile(foundSnippetPath, 'utf-8'); languageInput = detectedLang; languageDetectedFromSnippet = true; console.error(`Loaded code from snippet: ${foundSnippetPath}`); } catch (readError) { throw new McpError(ErrorCode.InternalError, `Failed to read snippet file '${foundSnippetPath}': ${readError instanceof Error ? readError.message : String(readError)}`); } if (!snippetFilename) { throw new McpError(ErrorCode.InternalError, `Could not determine snippet filename for ${snippet_name}`); } finalEntrypointFilename = snippetFilename; } else { languageInput = parseResult.data.language!; codeInput = parseResult.data.entrypoint_code!; finalEntrypointFilename = entrypoint_filename || ""; if (!finalEntrypointFilename) { switch(languageInput.toLowerCase()) { case "python": finalEntrypointFilename = "main.py"; break; case "go": finalEntrypointFilename = "main.go"; break; case "javascript": finalEntrypointFilename = "index.js"; break; default: throw new McpError(ErrorCode.InternalError, `Cannot determine default entrypoint filename for ${languageInput}`); } } } const langLower = languageInput.toLowerCase(); if (!serverConfig.languageSettings[langLower]) { const errorMsg = languageDetectedFromSnippet ? `Language '${languageInput}' (detected from snippet '${snippet_name}') is not configured.` : `Unsupported language provided directly: ${languageInput}`; throw new McpError(ErrorCode.InvalidParams, errorMsg); } // --- Temporary Directory and File Creation --- let tempDir: string | undefined; try { tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "isolator-mcp-")); // Set directory permissions to be accessible (rwxr-xr-x) await fs.chmod(tempDir, 0o755); console.error(`Created temp dir: ${tempDir} with mode 755`); // Write entrypoint file const entrypointPath = path.join(tempDir, finalEntrypointFilename); await fs.writeFile(entrypointPath, codeInput); // Set file permissions to be readable (rw-r--r--) // Use 0o755 if script needs to be executable itself by the container user (less common) await fs.chmod(entrypointPath, 0o644); console.error(`Wrote entrypoint file: ${entrypointPath} with mode 644`); // Write additional files if (additional_files) { for (const file of additional_files) { const safeFilename = path.basename(file.filename); if (safeFilename !== file.filename || safeFilename === "." || safeFilename === "..") { throw new McpError(ErrorCode.InvalidParams, `Invalid additional filename: ${file.filename}`); } const additionalFilePath = path.join(tempDir, safeFilename); await fs.writeFile(additionalFilePath, file.content); // Set file permissions to be readable (rw-r--r--) await fs.chmod(additionalFilePath, 0o644); console.error(`Wrote additional file: ${additionalFilePath} with mode 644`); } } // --- Construct Go CLI Command --- const args: string[] = [ "run", "--language", langLower, "--dir", tempDir, "--timeout", `${serverConfig.defaultTimeoutSeconds}s`, "--memory", String(serverConfig.defaultMemoryMB), "--cpus", String(serverConfig.defaultCpus), "--pids-limit", String(serverConfig.defaultPidsLimit), "--network", serverConfig.defaultNetworkMode, "--read-only", String(serverConfig.defaultReadOnlyRootfs), "--container-workdir", serverConfig.defaultContainerWorkdir, "--entrypoint", finalEntrypointFilename, ...serverConfig.defaultCapDrop.map(cap => ["--cap-drop", cap]).flat(), ]; // Add GOCACHE env var specifically for Go if (langLower === "go") { args.push("--env", "GOCACHE=/tmp"); } console.error(`Executing: ${serverConfig.isolatorPath} ${args.join(" ")}`); // --- Execute Go CLI --- const executionPromise = new Promise<ExecutionResult>((resolve, reject) => { // Use default spawn options, relying on the --env flag now const spawnOptions: Parameters<typeof spawn>[2] = { stdio: ["ignore", "pipe", "pipe"], env: process.env }; const child = spawn(serverConfig.isolatorPath, args, spawnOptions); let stdoutData = ""; let stderrData = ""; if (child.stdout) { child.stdout.on("data", (data) => { stdoutData += data.toString(); }); } else { console.error("Warning: child.stdout is null"); } if (child.stderr) { child.stderr.on("data", (data) => { stderrData += data.toString(); console.error("isolator CLI stderr:", data.toString()); }); } else { console.error("Warning: child.stderr is null"); } child.on("error", (err) => { console.error("Failed to start isolator CLI:", err); reject(new Error(`Failed to start isolator process: ${err.message}`)); }); child.on("close", (code) => { console.error(`isolator CLI exited with code ${code}`); try { const result: ExecutionResult = JSON.parse(stdoutData); resolve(result); } catch (parseError) { console.error("Failed to parse isolator JSON output:", parseError); console.error("isolator stdout:", stdoutData); resolve({ status: "error", exitCode: code ?? -1, stdout: stdoutData, stderr: stderrData, durationMs: 0, error: `Failed to parse execution result from isolator CLI. Exit code: ${code}. ${parseError instanceof Error ? parseError.message : String(parseError)}` }); } }); }); const result = await executionPromise; // --- Format MCP Result --- const outputText = `--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`; const content: TextContent = { type: "text", text: outputText }; const callResult: CallToolResult = { content: [content] }; if (result.status !== "success") { callResult.isError = true; content.text = `Execution Failed (${result.status}): ${result.error || 'Unknown error'}\n\n` + content.text; } return callResult; } catch (error) { console.error("Error during execute_code handling:", error); throw new McpError(ErrorCode.InternalError, `Failed to execute code: ${error instanceof Error ? error.message : String(error)}`); } finally { // --- Cleanup Temporary Directory --- if (tempDir) { try { await fs.rm(tempDir, { recursive: true, force: true }); console.error(`Cleaned up temp directory: ${tempDir}`); } catch (cleanupError) { console.error(`Error cleaning up temp directory ${tempDir}:`, cleanupError); } } } }); // --- Server Startup --- async function main() { try { serverConfig = await loadConfig(); // Load config before connecting server.onerror = (error) => console.error("[MCP Error]", error); process.on("SIGINT", async () => { console.error("Received SIGINT, shutting down server..."); await server.close(); process.exit(0); }); const transport = new StdioServerTransport(); await server.connect(transport); console.error("Isolator MCP server running on stdio"); } catch (error) { console.error("Failed to start Isolator MCP server:", error); process.exit(1); } } main();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ompragash/isolator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server