Skip to main content
Glama

Isolator MCP Server

by Ompragash
run.go17.1 kB
package cmd import ( "archive/tar" // Needed for CopyToContainer "bytes" "context" "encoding/json" "fmt" "io" // Needed for CopyToContainer "os" // Needed for CopyToContainer path manipulation "path/filepath" "strings" "time" "github.com/docker/docker/api/types/container" // "github.com/docker/docker/api/types/mount" // REMOVED mount import "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/spf13/cobra" ) // Result structure for JSON output type ExecutionResult struct { Status string `json:"status"` // "success", "timeout", "error" ExitCode int64 `json:"exitCode"` Stdout string `json:"stdout"` Stderr string `json:"stderr"` DurationMs int64 `json:"durationMs"` Error string `json:"error,omitempty"` // Only present if status is "error" or "timeout" ImageTag string `json:"imageTag,omitempty"` // Only if --save-image was used } // Flags for the run command var ( language string dirPath string timeout time.Duration runtime string readOnlyRootfs bool networkMode string capDrop []string pidsLimit int64 memoryLimit int64 // in MB cpusLimit float64 saveImageTag string saveDockerfile string containerWorkdir string // Will be read from server config later, needed for copy target entrypointFile string // Added flag for explicit entrypoint filename envVars []string // New flag for environment variables ) // runCmd represents the run command var runCmd = &cobra.Command{ Use: "run", Short: "Run code in an isolated environment", Long: `Executes code provided in a specified directory within a secure container. Supports Python, Go, and JavaScript. Enforces security constraints and resource limits.`, RunE: func(cmd *cobra.Command, args []string) error { startTime := time.Now() // Basic validation if dirPath == "" { return fmt.Errorf("--dir flag is required") } // No Abs path needed for copy source // absDirPath, err := filepath.Abs(dirPath) // if err != nil { // return fmt.Errorf("failed to get absolute path for --dir: %w", err) // } // dirPath = absDirPath if language == "" { return fmt.Errorf("--language flag is required") } if containerWorkdir == "" { return fmt.Errorf("--container-workdir flag is required (this should be passed by the caller)") } if entrypointFile == "" { // Add check for required entrypoint flag return fmt.Errorf("--entrypoint flag is required") } // --- Log received flags --- fmt.Fprintf(os.Stderr, "[isolator CLI DEBUG] Received flags:\n") fmt.Fprintf(os.Stderr, " --language: %s\n", language) fmt.Fprintf(os.Stderr, " --dir: %s\n", dirPath) fmt.Fprintf(os.Stderr, " --entrypoint: %s\n", entrypointFile) // Log the crucial flag fmt.Fprintf(os.Stderr, " --timeout: %s\n", timeout) fmt.Fprintf(os.Stderr, " --container-workdir: %s\n", containerWorkdir) // --- End Log --- var execResult ExecutionResult execResult.Status = "error" // Default to error // --- Docker Execution Logic --- ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { execResult.Error = fmt.Sprintf("Failed to create docker client: %s", err.Error()) outputJSON(execResult, startTime) // Attempt to output error return fmt.Errorf("failed to create docker client: %w", err) } defer cli.Close() // Get base image based on language image, err := getLangImage(language) // Changed helper function if err != nil { execResult.Error = err.Error() outputJSON(execResult, startTime) return err } // Construct the command using the provided entrypointFile flag var execCmd string // The actual command string to run var containerCmd []string // The command string slice switch strings.ToLower(language) { case "python": containerCmd = []string{"python", entrypointFile} // Use relative path execCmd = fmt.Sprintf("python %s", entrypointFile) case "go": containerCmd = []string{"go", "run", entrypointFile} // Keep relative for Go execCmd = fmt.Sprintf("go run %s", entrypointFile) case "javascript", "js", "node": containerCmd = []string{"node", entrypointFile} // Use relative path execCmd = fmt.Sprintf("node %s", entrypointFile) default: return fmt.Errorf("logic error: unsupported language %s reached command construction", language) } // Check if entrypoint file actually exists IN THE SOURCE DIR entrypointPathOnHost := filepath.Join(dirPath, entrypointFile) if _, err := os.Stat(entrypointPathOnHost); os.IsNotExist(err) { errMsg := fmt.Sprintf("Entrypoint file '%s' not found in source directory '%s'", entrypointFile, dirPath) execResult.Error = errMsg outputJSON(execResult, startTime) return fmt.Errorf(errMsg) } // Wrap the command in a shell to add debugging 'ls /' and 'ls /workspace' // Keep this debug for now, though ls might fail initially before copy debugCmd := fmt.Sprintf("echo '--- ls / ---' && ls -al / && echo '--- ls /workspace (before copy) ---' && ls -al %s; echo '--- Running Command ---' && %s", containerWorkdir, execCmd) containerShellCmd := []string{"sh", "-c", debugCmd} // Create container config (NO WorkingDir initially, command uses relative path assuming copy destination) containerCfg := &container.Config{ Image: image, Cmd: containerShellCmd, // Use shell wrapper for debug ls WorkingDir: containerWorkdir, // Set working dir where files will be copied Env: envVars, // Pass environment variables from flag Tty: false, AttachStdout: true, AttachStderr: true, // User: default non-root user } // Create host config (NO Mounts) hostCfg := &container.HostConfig{ Resources: container.Resources{ Memory: memoryLimit * 1024 * 1024, NanoCPUs: int64(cpusLimit * 1e9), PidsLimit: &pidsLimit, }, NetworkMode: container.NetworkMode(networkMode), ReadonlyRootfs: readOnlyRootfs, // Keep rootfs read-only if possible // Mounts: REMOVED CapDrop: capDrop, SecurityOpt: []string{"no-new-privileges:true"}, } // Create execution context with timeout execCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() // Create container resp, err := cli.ContainerCreate(execCtx, containerCfg, hostCfg, nil, nil, "") if err != nil { execResult.Error = fmt.Sprintf("Failed to create container: %s", err.Error()) outputJSON(execResult, startTime) return fmt.Errorf("failed to create container: %w", err) } containerID := resp.ID // Ensure container cleanup defer func() { cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second) defer cleanupCancel() _ = cli.ContainerRemove(cleanupCtx, containerID, container.RemoveOptions{Force: true, RemoveVolumes: true}) }() // --- Copy files into the STARTING container --- // Create a tar archive of the source directory contents tarReader, err := createTarArchive(dirPath) if err != nil { execResult.Error = fmt.Sprintf("Failed to create tar archive for copying: %s", err.Error()) outputJSON(execResult, startTime) // Try to remove container before erroring cli.ContainerRemove(context.Background(), containerID, container.RemoveOptions{Force: true}) return fmt.Errorf("failed to create tar archive: %w", err) } // No need to defer tarReader.Close() because CopyToContainer consumes it // Copy the archive to the container's working directory // Use a separate context for copy operation if needed, but execCtx should be okay err = cli.CopyToContainer(execCtx, containerID, containerWorkdir, tarReader, container.CopyToContainerOptions{ AllowOverwriteDirWithFile: false, CopyUIDGID: false, // Let ownership be default inside container }) if err != nil { execResult.Error = fmt.Sprintf("Failed to copy files to container: %s", err.Error()) outputJSON(execResult, startTime) // Try to remove container before erroring cli.ContainerRemove(context.Background(), containerID, container.RemoveOptions{Force: true}) return fmt.Errorf("failed to copy files to container: %w", err) } fmt.Fprintf(os.Stderr, "[isolator CLI DEBUG] Copied files from %s to container %s:%s\n", dirPath, containerID, containerWorkdir) // Start the container (AFTER copying files) if err := cli.ContainerStart(execCtx, containerID, container.StartOptions{}); err != nil { execResult.Error = fmt.Sprintf("Failed to start container: %s", err.Error()) outputJSON(execResult, startTime) return fmt.Errorf("failed to start container: %w", err) } // --- Wait for completion and gather results --- statusCh, errCh := cli.ContainerWait(execCtx, containerID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil && !strings.Contains(err.Error(), "context deadline exceeded") { execResult.Error = fmt.Sprintf("Error waiting for container: %s", err.Error()) } case status := <-statusCh: execResult.ExitCode = status.StatusCode if status.StatusCode == 0 { execResult.Status = "success" } else { execResult.Status = "error" } case <-execCtx.Done(): execResult.Status = "timeout" execResult.Error = fmt.Sprintf("Execution timed out after %s", timeout) } // --- Get logs regardless of status --- logCtx, logCancel := context.WithTimeout(context.Background(), 10*time.Second) defer logCancel() logsReader, logErr := cli.ContainerLogs(logCtx, containerID, container.LogsOptions{ ShowStdout: true, ShowStderr: true, Timestamps: false, Follow: false, }) // Handle log retrieval error AFTER processing main execution status/error var stdoutStr, stderrStr string if logErr == nil { defer logsReader.Close() stdoutBuf := new(bytes.Buffer) stderrBuf := new(bytes.Buffer) _, copyErr := stdcopy.StdCopy(stdoutBuf, stderrBuf, logsReader) if copyErr != nil { fmt.Fprintf(os.Stderr, "[isolator CLI DEBUG] Error reading container logs: %s\n", copyErr.Error()) } stdoutStr = stdoutBuf.String() stderrStr = stderrBuf.String() } else { fmt.Fprintf(os.Stderr, "[isolator CLI DEBUG] Error getting container logs: %s\n", logErr.Error()) } execResult.Stdout = stdoutStr execResult.Stderr = stderrStr // Refine error message if needed if execResult.Status == "error" && execResult.Error == "" { trimmedStderr := strings.TrimSpace(stderrStr) // Check if stderr contains the debug output prefix, ignore it for the main error debugPrefix := "echo '--- ls / ---'" if strings.HasPrefix(trimmedStderr, debugPrefix) { // Find the actual error message after the debug output parts := strings.SplitN(trimmedStderr, "--- Running Command ---", 2) if len(parts) > 1 { actualStderr := strings.TrimSpace(parts[1]) if actualStderr != "" { execResult.Error = actualStderr } } } else if trimmedStderr != "" { execResult.Error = trimmedStderr } if execResult.Error == "" { // Fallback if stderr was empty or only debug execResult.Error = fmt.Sprintf("Container exited with status code %d", execResult.ExitCode) } } // --- Handle saving image (optional) --- if execResult.Status == "success" && saveImageTag != "" { commitCtx, commitCancel := context.WithTimeout(context.Background(), 30*time.Second) defer commitCancel() _, err := cli.ContainerCommit(commitCtx, containerID, container.CommitOptions{Reference: saveImageTag}) if err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to commit container image %s: %s\n", saveImageTag, err.Error()) } else { execResult.ImageTag = saveImageTag } } // --- Handle saving dockerfile (optional) --- if saveDockerfile != "" { baseImage, _ := getLangImage(language) dockerfileContent := fmt.Sprintf("FROM %s\nWORKDIR %s\n# Add COPY commands for files in %s if needed\nCMD [\"%s\"]", baseImage, containerWorkdir, containerWorkdir, strings.Join(containerCmd, "\", \"")) // Use containerCmd err := os.WriteFile(saveDockerfile, []byte(dockerfileContent), 0644) if err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to save Dockerfile to %s: %s\n", saveDockerfile, err.Error()) } } // --- Finalize and output JSON --- outputJSON(execResult, startTime) return nil // Success/failure indicated in JSON output }, } // Helper function to get Docker image based on language func getLangImage(language string) (image string, err error) { switch strings.ToLower(language) { case "python": image = "python:3.11-alpine" case "go": image = "golang:1.21-alpine" case "javascript", "js", "node": image = "node:20-alpine" default: err = fmt.Errorf("unsupported language: %s", language) } return } // createTarArchive creates a reader for a tar archive containing the files in the source directory. func createTarArchive(sourceDir string) (io.ReadCloser, error) { pr, pw := io.Pipe() // Use a pipe to stream tar data tw := tar.NewWriter(pw) go func() { defer pw.Close() // Close writer when done defer tw.Close() // Close tar writer // Walk the source directory err := filepath.Walk(sourceDir, func(file string, fi os.FileInfo, err error) error { if err != nil { return err } // Skip the root directory itself if file == sourceDir { return nil } // Create tar header header, err := tar.FileInfoHeader(fi, fi.Name()) // Use relative path within archive if err != nil { return fmt.Errorf("failed to create tar header for %s: %w", file, err) } // Adjust header name to be relative to the sourceDir root for tar structure relPath, err := filepath.Rel(sourceDir, file) if err != nil { return fmt.Errorf("failed to get relative path for %s: %w", file, err) } header.Name = relPath // Use relative path in tar archive // Write header if err := tw.WriteHeader(header); err != nil { return fmt.Errorf("failed to write tar header for %s: %w", file, err) } // If not a directory, write file content if !fi.IsDir() { f, err := os.Open(file) if err != nil { return fmt.Errorf("failed to open file %s: %w", file, err) } defer f.Close() if _, err := io.Copy(tw, f); err != nil { return fmt.Errorf("failed to copy file content for %s: %w", file, err) } } return nil }) if err != nil { // Close the pipe writer with an error if Walk failed _ = pw.CloseWithError(fmt.Errorf("error during tar archiving: %w", err)) } }() return pr, nil // Return the reader part of the pipe } // Helper function to output JSON result func outputJSON(result ExecutionResult, startTime time.Time) { result.DurationMs = time.Since(startTime).Milliseconds() jsonOutput, err := json.Marshal(result) if err != nil { fmt.Fprintf(os.Stderr, "Internal Error: Failed to marshal result to JSON: %s\n", err.Error()) fmt.Printf(`{"status": "error", "error": "Internal Error: Failed to marshal result to JSON", "durationMs": %d}`, result.DurationMs) return } fmt.Println(string(jsonOutput)) } func init() { rootCmd.AddCommand(runCmd) // Define flags runCmd.Flags().StringVarP(&language, "language", "l", "", "Language of the code (python, go, javascript)") runCmd.Flags().StringVarP(&dirPath, "dir", "d", "", "Path to the host directory containing input files") runCmd.Flags().DurationVarP(&timeout, "timeout", "t", 60*time.Second, "Execution timeout") runCmd.Flags().StringVar(&runtime, "runtime", "docker", "Container runtime to use (docker)") runCmd.Flags().BoolVar(&readOnlyRootfs, "read-only", true, "Mount container root filesystem as read-only") runCmd.Flags().StringVar(&networkMode, "network", "none", "Container network mode (e.g., none, bridge)") runCmd.Flags().StringSliceVar(&capDrop, "cap-drop", []string{"ALL"}, "Linux capabilities to drop") runCmd.Flags().Int64Var(&pidsLimit, "pids-limit", 64, "Maximum number of processes allowed in the container") runCmd.Flags().Int64VarP(&memoryLimit, "memory", "m", 256, "Memory limit in MB") runCmd.Flags().Float64VarP(&cpusLimit, "cpus", "c", 0.5, "CPU limit (e.g., 0.5 for half a core)") runCmd.Flags().StringVar(&containerWorkdir, "container-workdir", "", "Working directory inside the container (target for copy)") runCmd.Flags().StringVar(&saveImageTag, "save-image", "", "Tag to save the container image as on success (e.g., my-image:v1)") runCmd.Flags().StringVar(&saveDockerfile, "save-dockerfile", "", "Path to save the base Dockerfile used") runCmd.Flags().StringVarP(&entrypointFile, "entrypoint", "e", "", "Filename of the entrypoint script within the directory") runCmd.Flags().StringSliceVar(&envVars, "env", []string{}, "Environment variables to set in the container (e.g., --env KEY1=VALUE1 --env KEY2=VALUE2)") // Add env flag // Mark required flags _ = runCmd.MarkFlagRequired("language") _ = runCmd.MarkFlagRequired("dir") _ = runCmd.MarkFlagRequired("container-workdir") _ = runCmd.MarkFlagRequired("entrypoint") }

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