# Directory Structure
```
├── .github
│ └── workflows
│ └── release.yml
├── .gitignore
├── build.sh
├── DEVELOPMENT.md
├── Dockerfile
├── install.ps1
├── install.sh
├── LICENSE
├── README.md
├── smithery.yaml
├── src
│ └── code-sandbox-mcp
│ ├── go.mod
│ ├── go.sum
│ ├── installer
│ │ ├── install.go
│ │ └── update.go
│ ├── main.go
│ ├── resources
│ │ └── container_logs.go
│ └── tools
│ ├── copy-file-from-container.go
│ ├── copy-file.go
│ ├── copy-project.go
│ ├── exec.go
│ ├── initialize.go
│ ├── stop-container.go
│ └── write-file.go
└── test
├── go
│ ├── go.mod
│ ├── go.sum
│ └── test.go
├── python
│ ├── main.py
│ └── requirements.txt
└── typescript
├── index.d.ts
├── package-lock.json
├── package.json
├── test.ts
└── types
└── test.d.ts
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | venv/
2 | .env
3 | bin/
4 | .DS_Store
5 | # Test directory
6 | test_code/*
7 | src/code-sandbox-mcp/vendor
8 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Code Sandbox MCP 🐳
2 | [](https://smithery.ai/server/@Automata-Labs-team/code-sandbox-mcp)
3 |
4 | A secure sandbox environment for executing code within Docker containers. This MCP server provides AI applications with a safe and isolated environment for running code while maintaining security through containerization.
5 |
6 | ## 🌟 Features
7 |
8 | - **Flexible Container Management**: Create and manage isolated Docker containers for code execution
9 | - **Custom Environment Support**: Use any Docker image as your execution environment
10 | - **File Operations**: Easy file and directory transfer between host and containers
11 | - **Command Execution**: Run any shell commands within the containerized environment
12 | - **Real-time Logging**: Stream container logs and command output in real-time
13 | - **Auto-Updates**: Built-in update checking and automatic binary updates
14 | - **Multi-Platform**: Supports Linux, macOS, and Windows
15 |
16 | ## 🚀 Installation
17 |
18 | ### Prerequisites
19 |
20 | - Docker installed and running
21 | - [Install Docker for Linux](https://docs.docker.com/engine/install/)
22 | - [Install Docker Desktop for macOS](https://docs.docker.com/desktop/install/mac/)
23 | - [Install Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/)
24 |
25 | ### Quick Install
26 |
27 | #### Linux, MacOS
28 | ```bash
29 | curl -fsSL https://raw.githubusercontent.com/Automata-Labs-team/code-sandbox-mcp/main/install.sh | bash
30 | ```
31 |
32 | #### Windows
33 | ```powershell
34 | # Run in PowerShell
35 | irm https://raw.githubusercontent.com/Automata-Labs-team/code-sandbox-mcp/main/install.ps1 | iex
36 | ```
37 |
38 | The installer will:
39 | 1. Check for Docker installation
40 | 2. Download the appropriate binary for your system
41 | 3. Create necessary configuration files
42 |
43 | ### Manual Installation
44 |
45 | 1. Download the latest release for your platform from the [releases page](https://github.com/Automata-Labs-team/code-sandbox-mcp/releases)
46 | 2. Place the binary in a directory in your PATH
47 | 3. Make it executable (Unix-like systems only):
48 | ```bash
49 | chmod +x code-sandbox-mcp
50 | ```
51 |
52 | ## 🛠️ Available Tools
53 |
54 | #### `sandbox_initialize`
55 | Initialize a new compute environment for code execution.
56 | Creates a container based on the specified Docker image.
57 |
58 | **Parameters:**
59 | - `image` (string, optional): Docker image to use as the base environment
60 | - Default: 'python:3.12-slim-bookworm'
61 |
62 | **Returns:**
63 | - `container_id` that can be used with other tools to interact with this environment
64 |
65 | #### `copy_project`
66 | Copy a directory to the sandboxed filesystem.
67 |
68 | **Parameters:**
69 | - `container_id` (string, required): ID of the container returned from the initialize call
70 | - `local_src_dir` (string, required): Path to a directory in the local file system
71 | - `dest_dir` (string, optional): Path to save the src directory in the sandbox environment
72 |
73 | #### `write_file`
74 | Write a file to the sandboxed filesystem.
75 |
76 | **Parameters:**
77 | - `container_id` (string, required): ID of the container returned from the initialize call
78 | - `file_name` (string, required): Name of the file to create
79 | - `file_contents` (string, required): Contents to write to the file
80 | - `dest_dir` (string, optional): Directory to create the file in (Default: ${WORKDIR})
81 |
82 | #### `sandbox_exec`
83 | Execute commands in the sandboxed environment.
84 |
85 | **Parameters:**
86 | - `container_id` (string, required): ID of the container returned from the initialize call
87 | - `commands` (array, required): List of command(s) to run in the sandboxed environment
88 | - Example: ["apt-get update", "pip install numpy", "python script.py"]
89 |
90 | #### `copy_file`
91 | Copy a single file to the sandboxed filesystem.
92 |
93 | **Parameters:**
94 | - `container_id` (string, required): ID of the container returned from the initialize call
95 | - `local_src_file` (string, required): Path to a file in the local file system
96 | - `dest_path` (string, optional): Path to save the file in the sandbox environment
97 |
98 | #### `sandbox_stop`
99 | Stop and remove a running container sandbox.
100 |
101 | **Parameters:**
102 | - `container_id` (string, required): ID of the container to stop and remove
103 |
104 | **Description:**
105 | Gracefully stops the specified container with a 10-second timeout and removes it along with its volumes.
106 |
107 | #### Container Logs Resource
108 | A dynamic resource that provides access to container logs.
109 |
110 | **Resource Path:** `containers://{id}/logs`
111 | **MIME Type:** `text/plain`
112 | **Description:** Returns all container logs from the specified container as a single text resource.
113 |
114 | ## 🔐 Security Features
115 |
116 | - Isolated execution environment using Docker containers
117 | - Resource limitations through Docker container constraints
118 | - Separate stdout and stderr streams
119 |
120 |
121 | ## 🔧 Configuration
122 |
123 | ### Claude Desktop
124 |
125 | The installer automatically creates the configuration file. If you need to manually configure it:
126 |
127 | #### Linux
128 | ```json
129 | // ~/.config/Claude/claude_desktop_config.json
130 | {
131 | "mcpServers": {
132 | "code-sandbox-mcp": {
133 | "command": "/path/to/code-sandbox-mcp",
134 | "args": [],
135 | "env": {}
136 | }
137 | }
138 | }
139 | ```
140 |
141 | #### macOS
142 | ```json
143 | // ~/Library/Application Support/Claude/claude_desktop_config.json
144 | {
145 | "mcpServers": {
146 | "code-sandbox-mcp": {
147 | "command": "/path/to/code-sandbox-mcp",
148 | "args": [],
149 | "env": {}
150 | }
151 | }
152 | }
153 | ```
154 |
155 | #### Windows
156 | ```json
157 | // %APPDATA%\Claude\claude_desktop_config.json
158 | {
159 | "mcpServers": {
160 | "code-sandbox-mcp": {
161 | "command": "C:\\path\\to\\code-sandbox-mcp.exe",
162 | "args": [],
163 | "env": {}
164 | }
165 | }
166 | }
167 | ```
168 |
169 | ### Other AI Applications
170 |
171 | For other AI applications that support MCP servers, configure them to use the `code-sandbox-mcp` binary as their code execution backend.
172 |
173 | ## 🛠️ Development
174 |
175 | If you want to build the project locally or contribute to its development, see [DEVELOPMENT.md](DEVELOPMENT.md).
176 |
177 | ## 📝 License
178 |
179 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
180 |
```
--------------------------------------------------------------------------------
/test/python/requirements.txt:
--------------------------------------------------------------------------------
```
1 | numpy
```
--------------------------------------------------------------------------------
/test/typescript/types/test.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | export {};
2 |
```
--------------------------------------------------------------------------------
/test/python/main.py:
--------------------------------------------------------------------------------
```python
1 | import numpy as np
2 |
3 | print(np.array([1, 2, 3]))
4 |
```
--------------------------------------------------------------------------------
/test/typescript/index.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | declare module '@automatalabs/mcp-server-playwright/dist/index.js' {
2 | export const Tools: any[];
3 | }
```
--------------------------------------------------------------------------------
/test/go/test.go:
--------------------------------------------------------------------------------
```go
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | cmd "github.com/docker/cli/cli/command"
7 | )
8 |
9 | func main() {
10 | fmt.Println("Hello, World!")
11 | os.Exit(0)
12 | cmd.PrettyPrint("Hello, World!")
13 | }
14 |
```
--------------------------------------------------------------------------------
/test/typescript/test.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Import the @automatalabs/mcp-server-playwright package
2 | import * as mcp from "@automatalabs/mcp-server-playwright/dist/index.js";
3 |
4 | console.log(JSON.stringify(mcp.Tools, ["name", "description"], 2));
5 |
```
--------------------------------------------------------------------------------
/test/typescript/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "test",
3 | "version": "1.0.0",
4 | "main": "test.ts",
5 | "scripts": {
6 | "test": "test.ts",
7 | "start": "node test.ts"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "description": "",
12 | "dependencies": {
13 | "@automatalabs/mcp-server-playwright": "^1.2.1"
14 | }
15 | }
16 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: sse
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required: []
9 | properties: {}
10 | commandFunction:
11 | # A function that produces the CLI command to start the MCP on stdio.
12 | |-
13 | (config) => ({command: '/usr/local/bin/code-sandbox-mcp', args: []})
14 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | # Use the official Go image as a build environment
3 | FROM golang:1.23.4-alpine3.21 AS builder
4 |
5 | # Install necessary packages
6 | RUN apk add --no-cache bash git make
7 |
8 | # Set the working directory
9 | WORKDIR /app
10 |
11 | # Copy the source code into the container
12 | COPY . .
13 |
14 | # Build the application using the build script
15 | RUN ./build.sh --release
16 |
17 | # Use a Docker in Docker image for running the application
18 | FROM docker:24-dind
19 |
20 | # Set the working directory
21 | WORKDIR /app
22 |
23 | # Copy the built binary from the builder stage
24 | COPY --from=builder /app/bin/code-sandbox-mcp /usr/local/bin/
25 |
26 | # Expose any ports the application needs
27 | EXPOSE 9520
28 |
29 | # Run the application
30 | ENTRYPOINT ["/bin/bash", "code-sandbox-mcp"]
31 |
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/resources/container_logs.go:
--------------------------------------------------------------------------------
```go
1 | package resources
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/docker/docker/api/types/container"
9 | "github.com/docker/docker/pkg/stdcopy"
10 |
11 | "github.com/docker/docker/client"
12 | "github.com/mark3labs/mcp-go/mcp"
13 | )
14 |
15 | func GetContainerLogs(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
16 |
17 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
18 | if err != nil {
19 | return nil, fmt.Errorf("failed to create Docker client: %w", err)
20 | }
21 | defer cli.Close()
22 |
23 | containerIDPath, found := strings.CutPrefix(request.Params.URI, "containers://") // Extract ID from the full URI
24 | if !found {
25 | return nil, fmt.Errorf("invalid URI: %s", request.Params.URI)
26 | }
27 | containerID := strings.TrimSuffix(containerIDPath, "/logs")
28 |
29 | // Set default ContainerLogsOptions
30 | logOpts := container.LogsOptions{
31 | ShowStdout: true,
32 | ShowStderr: true,
33 | }
34 |
35 | // Actually fetch the logs
36 | reader, err := cli.ContainerLogs(ctx, containerID, logOpts)
37 | if err != nil {
38 | return nil, fmt.Errorf("error fetching container logs: %w", err)
39 | }
40 | defer reader.Close()
41 |
42 | var b strings.Builder
43 | if _, err := stdcopy.StdCopy(&b, &b, reader); err != nil {
44 | return nil, fmt.Errorf("error copying container logs: %w", err)
45 | }
46 |
47 | // Combine them. You could also return them separately if you prefer.
48 | combined := b.String()
49 |
50 | return []mcp.ResourceContents{
51 | mcp.TextResourceContents{
52 | URI: fmt.Sprintf("containers://%s/logs", containerID),
53 | MIMEType: "text/plain",
54 | Text: combined,
55 | },
56 | }, nil
57 | }
58 |
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/tools/stop-container.go:
--------------------------------------------------------------------------------
```go
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/docker/docker/api/types/container"
8 | "github.com/docker/docker/client"
9 | "github.com/mark3labs/mcp-go/mcp"
10 | )
11 |
12 | // StopContainer stops and removes a container by its ID
13 | func StopContainer(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
14 | // Get the container ID from the request
15 | containerId, ok := request.Params.Arguments["container_id"].(string)
16 | if !ok || containerId == "" {
17 | return mcp.NewToolResultText("Error: container_id is required"), nil
18 | }
19 |
20 | // Stop and remove the container
21 | if err := stopAndRemoveContainer(ctx, containerId); err != nil {
22 | return mcp.NewToolResultText(fmt.Sprintf("Error: %v", err)), nil
23 | }
24 |
25 | return mcp.NewToolResultText(fmt.Sprintf("Successfully stopped and removed container: %s", containerId)), nil
26 | }
27 |
28 | // stopAndRemoveContainer stops and removes a Docker container
29 | func stopAndRemoveContainer(ctx context.Context, containerId string) error {
30 | cli, err := client.NewClientWithOpts(
31 | client.FromEnv,
32 | client.WithAPIVersionNegotiation(),
33 | )
34 | if err != nil {
35 | return fmt.Errorf("failed to create Docker client: %w", err)
36 | }
37 | defer cli.Close()
38 |
39 | // Stop the container with a timeout
40 | timeout := 10 // seconds
41 | if err := cli.ContainerStop(ctx, containerId, container.StopOptions{Timeout: &timeout}); err != nil {
42 | return fmt.Errorf("failed to stop container: %w", err)
43 | }
44 |
45 | // Remove the container
46 | if err := cli.ContainerRemove(ctx, containerId, container.RemoveOptions{
47 | RemoveVolumes: true,
48 | Force: true,
49 | }); err != nil {
50 | return fmt.Errorf("failed to remove container: %w", err)
51 | }
52 |
53 | return nil
54 | }
55 |
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/tools/initialize.go:
--------------------------------------------------------------------------------
```go
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | dockerImage "github.com/docker/docker/api/types/image"
8 | "github.com/docker/docker/api/types/container"
9 | "github.com/docker/docker/client"
10 | "github.com/mark3labs/mcp-go/mcp"
11 | )
12 |
13 | // InitializeEnvironment creates a new container for code execution
14 | func InitializeEnvironment(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
15 | // Get the requested Docker image or use default
16 | image, ok := request.Params.Arguments["image"].(string)
17 | if !ok || image == "" {
18 | // Default to a slim debian image with Python pre-installed
19 | image = "python:3.12-slim-bookworm"
20 | }
21 |
22 | // Create and start the container
23 | containerId, err := createContainer(ctx, image)
24 | if err != nil {
25 | return mcp.NewToolResultText(fmt.Sprintf("Error: %v", err)), nil
26 | }
27 |
28 | return mcp.NewToolResultText(fmt.Sprintf("container_id: %s", containerId)), nil
29 | }
30 |
31 | // createContainer creates a new Docker container and returns its ID
32 | func createContainer(ctx context.Context, image string) (string, error) {
33 | cli, err := client.NewClientWithOpts(
34 | client.FromEnv,
35 | client.WithAPIVersionNegotiation(),
36 | )
37 | if err != nil {
38 | return "", fmt.Errorf("failed to create Docker client: %w", err)
39 | }
40 | defer cli.Close()
41 |
42 | // Pull the Docker image if not already available
43 | reader, err := cli.ImagePull(ctx, image, dockerImage.PullOptions{})
44 | if err != nil {
45 | return "", fmt.Errorf("failed to pull Docker image %s: %w", image, err)
46 | }
47 | defer reader.Close()
48 |
49 | // Create container config with a working directory
50 | config := &container.Config{
51 | Image: image,
52 | WorkingDir: "/app",
53 | Tty: true,
54 | OpenStdin: true,
55 | StdinOnce: false,
56 | }
57 |
58 | // Create host config
59 | hostConfig := &container.HostConfig{
60 | // Add any resource constraints here if needed
61 | }
62 |
63 | // Create the container
64 | resp, err := cli.ContainerCreate(
65 | ctx,
66 | config,
67 | hostConfig,
68 | nil,
69 | nil,
70 | "",
71 | )
72 | if err != nil {
73 | return "", fmt.Errorf("failed to create container: %w", err)
74 | }
75 |
76 | // Start the container
77 | if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
78 | return "", fmt.Errorf("failed to start container: %w", err)
79 | }
80 |
81 | return resp.ID, nil
82 | }
83 |
```
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
```markdown
1 | # Development Guide 🛠️
2 |
3 | This guide is for developers who want to build the project locally or contribute to its development.
4 |
5 | ## Prerequisites
6 |
7 | - Go 1.21 or later
8 | - Docker installed and running
9 | - Git (for version information)
10 | - Make (optional, for build automation)
11 |
12 | ## Building from Source
13 |
14 | 1. Clone the repository:
15 | ```bash
16 | git clone https://github.com/Automata-Labs-team/code-sandbox-mcp.git
17 | cd code-sandbox-mcp
18 | ```
19 |
20 | 2. Build the project:
21 | ```bash
22 | # Development build
23 | ./build.sh
24 |
25 | # Release build
26 | ./build.sh --release
27 |
28 | # Release with specific version
29 | ./build.sh --release --version v1.0.0
30 | ```
31 |
32 | The binaries will be available in the `bin` directory.
33 |
34 | ## Build Options
35 |
36 | The `build.sh` script supports several options:
37 |
38 | | Option | Description |
39 | |--------|-------------|
40 | | `--release` | Build in release mode with version information |
41 | | `--version <ver>` | Specify a version number (e.g., v1.0.0) |
42 |
43 | ## Project Structure
44 |
45 | ```
46 | code-sandbox-mcp/
47 | ├── src/
48 | │ └── code-sandbox-mcp/
49 | │ └── main.go # Main application code
50 | ├── bin/ # Compiled binaries
51 | ├── build.sh # Build script
52 | ├── install.sh # Unix-like systems installer
53 | ├── install.ps1 # Windows installer
54 | ├── README.md # User documentation
55 | └── DEVELOPMENT.md # This file
56 | ```
57 |
58 | ## API Documentation
59 |
60 | The project implements the MCP (Machine Code Protocol) server interface for executing code in Docker containers.
61 |
62 | ### Core Functions
63 |
64 | - `runInDocker`: Executes single-file code in a Docker container
65 | - `runProjectInDocker`: Runs project directories in containers
66 | - `RegisterTool`: Registers new tool endpoints
67 | - `NewServer`: Creates a new MCP server instance
68 |
69 | ### Tool Arguments
70 |
71 | #### RunCodeArguments
72 | ```go
73 | type RunCodeArguments struct {
74 | Code string `json:"code"` // The code to run
75 | Language Language `json:"language"` // Programming language
76 | }
77 | ```
78 |
79 | #### RunProjectArguments
80 | ```go
81 | type RunProjectArguments struct {
82 | ProjectDir string `json:"project_dir"` // Project directory
83 | Language Language `json:"language"` // Programming language
84 | Entrypoint string `json:"entrypoint"` // Command to run the project
85 | Background bool `json:"background"` // Run in background
86 | }
87 | ```
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/installer/install.go:
--------------------------------------------------------------------------------
```go
1 | package installer
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "runtime"
9 | )
10 |
11 | // MCPConfig represents the Claude Desktop config file structure
12 | type MCPConfig struct {
13 | MCPServers map[string]MCPServer `json:"mcpServers"`
14 | }
15 |
16 | // MCPServer represents a single MCP server configuration
17 | type MCPServer struct {
18 | Command string `json:"command"`
19 | Args []string `json:"args"`
20 | Env map[string]string `json:"env"`
21 | }
22 | func InstallConfig() error {
23 | configPath, err := getConfigPath()
24 | if err != nil {
25 | return err
26 | }
27 |
28 | // Create config directory if it doesn't exist
29 | configDir := filepath.Dir(configPath)
30 | if err := os.MkdirAll(configDir, 0755); err != nil {
31 | return fmt.Errorf("failed to create config directory: %w", err)
32 | }
33 |
34 | // Get the absolute path of the current executable
35 | execPath, err := os.Executable()
36 | if err != nil {
37 | return fmt.Errorf("failed to get executable path: %w", err)
38 | }
39 | execPath, err = filepath.Abs(execPath)
40 | if err != nil {
41 | return fmt.Errorf("failed to get absolute path: %w", err)
42 | }
43 |
44 | var config MCPConfig
45 | if _, err := os.Stat(configPath); err == nil {
46 | // Read existing config
47 | configData, err := os.ReadFile(configPath)
48 | if err != nil {
49 | return fmt.Errorf("failed to read config file: %w", err)
50 | }
51 | if err := json.Unmarshal(configData, &config); err != nil {
52 | return fmt.Errorf("failed to parse config file: %w", err)
53 | }
54 | } else {
55 | // Create new config
56 | config = MCPConfig{
57 | MCPServers: make(map[string]MCPServer),
58 | }
59 | }
60 |
61 | // Add or update our server config
62 | var command string
63 | if runtime.GOOS == "windows" {
64 | command = "cmd"
65 | config.MCPServers["code-sandbox-mcp"] = MCPServer{
66 | Command: command,
67 | Args: []string{"/c", execPath},
68 | Env: map[string]string{},
69 | }
70 | } else {
71 | config.MCPServers["code-sandbox-mcp"] = MCPServer{
72 | Command: execPath,
73 | Args: []string{},
74 | Env: map[string]string{},
75 | }
76 | }
77 |
78 | // Write the updated config
79 | configData, err := json.MarshalIndent(config, "", " ")
80 | if err != nil {
81 | return fmt.Errorf("failed to marshal config: %w", err)
82 | }
83 |
84 | if err := os.WriteFile(configPath, configData, 0644); err != nil {
85 | return fmt.Errorf("failed to write config file: %w", err)
86 | }
87 |
88 | fmt.Printf("Added code-sandbox-mcp to %s\n", configPath)
89 | return nil
90 | }
91 |
92 | func getConfigPath() (string, error) {
93 | homeDir, err := os.UserHomeDir()
94 | if err != nil {
95 | return "", fmt.Errorf("failed to get user home directory: %w", err)
96 | }
97 |
98 | var configDir string
99 | switch runtime.GOOS {
100 | case "darwin":
101 | configDir = filepath.Join(homeDir, "Library", "Application Support", "Claude")
102 | case "windows":
103 | configDir = filepath.Join(os.Getenv("APPDATA"), "Claude")
104 | default: // linux and others
105 | configDir = filepath.Join(homeDir, ".config", "Claude")
106 | }
107 |
108 | return filepath.Join(configDir, "claude_desktop_config.json"), nil
109 | }
```
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # Default values
4 | VERSION="dev"
5 | RELEASE=false
6 |
7 | # Parse command line arguments
8 | while [[ "$#" -gt 0 ]]; do
9 | case $1 in
10 | --release)
11 | RELEASE=true
12 | # If no version specified, use git tag or commit hash
13 | if [ "$VERSION" = "dev" ]; then
14 | if [ -d .git ]; then
15 | VERSION=$(git describe --tags 2>/dev/null || git rev-parse --short HEAD)
16 | fi
17 | fi
18 | ;;
19 | --version)
20 | VERSION="$2"
21 | shift
22 | ;;
23 | *) echo "Unknown parameter: $1"; exit 1 ;;
24 | esac
25 | shift
26 | done
27 |
28 | # Create bin directory if it doesn't exist
29 | mkdir -p bin
30 |
31 | # Colors for output
32 | GREEN='\033[0;32m'
33 | RED='\033[0;31m'
34 | BLUE='\033[0;34m'
35 | NC='\033[0m' # No Color
36 |
37 | # Build mode banner
38 | if [ "$RELEASE" = true ]; then
39 | echo -e "${BLUE}Building in RELEASE mode (version: ${VERSION})${NC}"
40 | else
41 | echo -e "${BLUE}Building in DEVELOPMENT mode${NC}"
42 | fi
43 |
44 | # Build flags for optimization
45 | BUILDFLAGS="-trimpath" # Remove file system paths from binary
46 |
47 | # Set up ldflags
48 | LDFLAGS="-s -w" # Strip debug information and symbol tables
49 | if [ "$RELEASE" = true ]; then
50 | # Add version information for release builds
51 | LDFLAGS="$LDFLAGS -X 'main.Version=$VERSION' -X 'main.BuildMode=release'"
52 | else
53 | LDFLAGS="$LDFLAGS -X 'main.BuildMode=development'"
54 | fi
55 |
56 | # Function to build for a specific platform
57 | build_for_platform() {
58 | local GOOS=$1
59 | local GOARCH=$2
60 | local EXTENSION=$3
61 | local OUTPUT="$(pwd)/bin/code-sandbox-mcp-${GOOS}-${GOARCH}${EXTENSION}"
62 |
63 | if [ "$RELEASE" = true ]; then
64 | OUTPUT="$(pwd)/bin/code-sandbox-mcp-${GOOS}-${GOARCH}${EXTENSION}"
65 | fi
66 |
67 | echo -e "${GREEN}Building for ${GOOS}/${GOARCH}...${NC}"
68 | pushd src/code-sandbox-mcp
69 | GOOS=$GOOS GOARCH=$GOARCH go build -ldflags="${LDFLAGS}" ${BUILDFLAGS} -o "$OUTPUT" .
70 |
71 | if [ $? -eq 0 ]; then
72 | popd
73 | echo -e "${GREEN}✓ Successfully built:${NC} $OUTPUT"
74 | # Create symlink for native platform
75 | if [ "$GOOS" = "$(go env GOOS)" ] && [ "$GOARCH" = "$(go env GOARCH)" ]; then
76 | local SYMLINK="bin/code-sandbox-mcp${EXTENSION}"
77 | ln -sf "$(basename $OUTPUT)" "$SYMLINK"
78 | echo -e "${GREEN}✓ Created symlink:${NC} $SYMLINK -> $OUTPUT"
79 | fi
80 | else
81 | popd
82 | echo -e "${RED}✗ Failed to build for ${GOOS}/${GOARCH}${NC}"
83 | return 1
84 | fi
85 | }
86 |
87 | # Clean previous builds
88 | echo -e "${GREEN}Cleaning previous builds...${NC}"
89 | rm -f bin/code-sandbox-mcp*
90 |
91 | # Build for Linux
92 | build_for_platform linux amd64 ""
93 | build_for_platform linux arm64 ""
94 |
95 | # Build for macOS
96 | build_for_platform darwin amd64 ""
97 | build_for_platform darwin arm64 ""
98 |
99 | # Build for Windows
100 | build_for_platform windows amd64 ".exe"
101 | build_for_platform windows arm64 ".exe"
102 |
103 | echo -e "\n${GREEN}Build process completed!${NC}"
```
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/sh
2 | set -e
3 |
4 | # Colors for output
5 | RED='\033[0;31m'
6 | GREEN='\033[0;32m'
7 | YELLOW='\033[1;33m'
8 | NC='\033[0m' # No Color
9 |
10 | # Check if we're in a terminal that supports colors
11 | if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors)" -ge 8 ]; then
12 | HAS_COLORS=1
13 | else
14 | HAS_COLORS=0
15 | # Reset color variables if colors are not supported
16 | RED=''
17 | GREEN=''
18 | YELLOW=''
19 | NC=''
20 | fi
21 |
22 | # Function to print colored output
23 | print_status() {
24 | local color=$1
25 | local message=$2
26 | if [ "$HAS_COLORS" = "1" ]; then
27 | printf "%b%s%b\n" "$color" "$message" "$NC"
28 | else
29 | printf "%s\n" "$message"
30 | fi
31 | }
32 |
33 | # Detect OS and architecture
34 | OS=$(uname -s | tr '[:upper:]' '[:lower:]')
35 | ARCH=$(uname -m)
36 |
37 | # Convert architecture to our naming scheme
38 | case "$ARCH" in
39 | x86_64) ARCH="amd64" ;;
40 | aarch64) ARCH="arm64" ;;
41 | arm64) ARCH="arm64" ;;
42 | *)
43 | print_status "$RED" "Unsupported architecture: $ARCH"
44 | exit 1
45 | ;;
46 | esac
47 |
48 | # Convert OS to our naming scheme
49 | case "$OS" in
50 | linux) OS="linux" ;;
51 | darwin) OS="darwin" ;;
52 | *)
53 | print_status "$RED" "Unsupported operating system: $OS"
54 | exit 1
55 | ;;
56 | esac
57 |
58 | # Check if Docker is installed
59 | if ! command -v docker >/dev/null 2>&1; then
60 | print_status "$RED" "Error: Docker is not installed"
61 | print_status "$YELLOW" "Please install Docker first:"
62 | echo " - For Linux: https://docs.docker.com/engine/install/"
63 | echo " - For macOS: https://docs.docker.com/desktop/install/mac/"
64 | exit 1
65 | fi
66 |
67 | # Check if Docker daemon is running
68 | if ! docker info >/dev/null 2>&1; then
69 | print_status "$RED" "Error: Docker daemon is not running"
70 | print_status "$YELLOW" "Please start Docker and try again"
71 | exit 1
72 | fi
73 |
74 | print_status "$GREEN" "Downloading latest release..."
75 |
76 | # Get the latest release URL
77 | LATEST_RELEASE_URL=$(curl -s https://api.github.com/repos/Automata-Labs-team/code-sandbox-mcp/releases/latest | grep "browser_download_url.*code-sandbox-mcp-$OS-$ARCH" | cut -d '"' -f 4)
78 |
79 | if [ -z "$LATEST_RELEASE_URL" ]; then
80 | print_status "$RED" "Error: Could not find release for $OS-$ARCH"
81 | exit 1
82 | fi
83 |
84 | # Create installation directory
85 | INSTALL_DIR="$HOME/.local/share/code-sandbox-mcp"
86 | mkdir -p "$INSTALL_DIR"
87 |
88 | # Download to a temporary file first
89 | TEMP_FILE="$INSTALL_DIR/code-sandbox-mcp.tmp"
90 | print_status "$GREEN" "Installing to $INSTALL_DIR/code-sandbox-mcp..."
91 |
92 | if ! curl -L "$LATEST_RELEASE_URL" -o "$TEMP_FILE"; then
93 | print_status "$RED" "Error: Failed to download the binary"
94 | rm -f "$TEMP_FILE"
95 | exit 1
96 | fi
97 |
98 | chmod +x "$TEMP_FILE"
99 |
100 | # Try to stop the existing process if it's running
101 | if [ -f "$INSTALL_DIR/code-sandbox-mcp" ]; then
102 | pkill -f "$INSTALL_DIR/code-sandbox-mcp" >/dev/null 2>&1 || true
103 | sleep 1 # Give it a moment to shut down
104 | fi
105 |
106 | # Move the temporary file to the final location
107 | if ! mv "$TEMP_FILE" "$INSTALL_DIR/code-sandbox-mcp"; then
108 | print_status "$RED" "Error: Failed to install the binary. Please ensure no instances are running and try again."
109 | rm -f "$TEMP_FILE"
110 | exit 1
111 | fi
112 |
113 | # Add to Claude Desktop config
114 | print_status "$GREEN" "Adding to Claude Desktop configuration..."
115 | "$INSTALL_DIR/code-sandbox-mcp" --install
116 |
117 | print_status "$GREEN" "Installation complete!"
118 | echo "You can now use code-sandbox-mcp with Claude Desktop or other AI applications."
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/installer/update.go:
--------------------------------------------------------------------------------
```go
1 | package installer
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | "os"
9 | "os/exec"
10 | "runtime"
11 | "strings"
12 | )
13 |
14 | // Version information (set by build flags)
15 | var (
16 | Version = "dev" // Version number (from git tag or specified)
17 | BuildMode = "development" // Build mode (development or release)
18 | )
19 |
20 | // checkForUpdate checks GitHub releases for a newer version
21 | func CheckForUpdate() (bool, string, error) {
22 | resp, err := http.Get("https://api.github.com/repos/Automata-Labs-team/code-sandbox-mcp/releases/latest")
23 | if err != nil {
24 | return false, "", fmt.Errorf("failed to check for updates: %w", err)
25 | }
26 | defer resp.Body.Close()
27 |
28 | var release struct {
29 | TagName string `json:"tag_name"`
30 | Assets []struct {
31 | Name string `json:"name"`
32 | BrowserDownloadURL string `json:"browser_download_url"`
33 | } `json:"assets"`
34 | }
35 |
36 | if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
37 | return false, "", fmt.Errorf("failed to parse release info: %w", err)
38 | }
39 |
40 | // Skip update check if we're on development version
41 | if Version == "dev" {
42 | return false, "", nil
43 | }
44 |
45 | // Compare versions (assuming semver format v1.2.3)
46 | if release.TagName > "v"+Version {
47 | // Find matching asset for current OS/arch
48 | suffix := fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH)
49 | if runtime.GOOS == "windows" {
50 | suffix += ".exe"
51 | }
52 | for _, asset := range release.Assets {
53 | if strings.HasSuffix(asset.Name, suffix) {
54 | return true, asset.BrowserDownloadURL, nil
55 | }
56 | }
57 | }
58 |
59 | return false, "", nil
60 | }
61 |
62 | // performUpdate downloads and replaces the current binary and restarts the process
63 | func PerformUpdate(downloadURL string) error {
64 | // Get current executable path
65 | execPath, err := os.Executable()
66 | if err != nil {
67 | return fmt.Errorf("failed to get executable path: %w", err)
68 | }
69 |
70 | // Download new version to temporary file
71 | tmpFile, err := os.CreateTemp("", "code-sandbox-mcp-update-*")
72 | if err != nil {
73 | return fmt.Errorf("failed to create temp file: %w", err)
74 | }
75 | defer os.Remove(tmpFile.Name())
76 |
77 | resp, err := http.Get(downloadURL)
78 | if err != nil {
79 | return fmt.Errorf("failed to download update: %w", err)
80 | }
81 | defer resp.Body.Close()
82 |
83 | if _, err := io.Copy(tmpFile, resp.Body); err != nil {
84 | return fmt.Errorf("failed to write update: %w", err)
85 | }
86 | tmpFile.Close()
87 |
88 | // Make temporary file executable
89 | if runtime.GOOS != "windows" {
90 | if err := os.Chmod(tmpFile.Name(), 0755); err != nil {
91 | return fmt.Errorf("failed to make update executable: %w", err)
92 | }
93 | }
94 |
95 | // Replace the current executable
96 | // On Windows, we need to move the current executable first
97 | if runtime.GOOS == "windows" {
98 | oldPath := execPath + ".old"
99 | if err := os.Rename(execPath, oldPath); err != nil {
100 | return fmt.Errorf("failed to rename current executable: %w", err)
101 | }
102 | defer os.Remove(oldPath)
103 | }
104 |
105 | if err := os.Rename(tmpFile.Name(), execPath); err != nil {
106 | return fmt.Errorf("failed to replace executable: %w", err)
107 | }
108 |
109 | // Start the new version and exit the current process
110 | args := os.Args[1:] // Keep all arguments except the program name
111 | cmd := exec.Command(execPath, args...)
112 | cmd.Stdin = os.Stdin
113 | cmd.Stdout = os.Stdout
114 | cmd.Stderr = os.Stderr
115 | if err := cmd.Start(); err != nil {
116 | return fmt.Errorf("failed to start new version: %w", err)
117 | }
118 |
119 | // Exit the current process
120 | os.Exit(0)
121 | return nil // Never reached, just for compiler
122 | }
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/tools/copy-file-from-container.go:
--------------------------------------------------------------------------------
```go
1 | package tools
2 |
3 | import (
4 | "archive/tar"
5 | "context"
6 | "fmt"
7 | "io"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 |
12 | "github.com/docker/docker/client"
13 | "github.com/mark3labs/mcp-go/mcp"
14 | )
15 |
16 | // CopyFileFromContainer copies a single file from a container's filesystem to the local filesystem
17 | func CopyFileFromContainer(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
18 | // Extract parameters
19 | containerID, ok := request.Params.Arguments["container_id"].(string)
20 | if !ok || containerID == "" {
21 | return mcp.NewToolResultText("container_id is required"), nil
22 | }
23 |
24 | containerSrcPath, ok := request.Params.Arguments["container_src_path"].(string)
25 | if !ok || containerSrcPath == "" {
26 | return mcp.NewToolResultText("container_src_path is required"), nil
27 | }
28 |
29 | // If container path doesn't start with /, prepend /app/
30 | if !strings.HasPrefix(containerSrcPath, "/") {
31 | containerSrcPath = filepath.Join("/app", containerSrcPath)
32 | }
33 |
34 | // Get the local destination path (optional parameter)
35 | localDestPath, ok := request.Params.Arguments["local_dest_path"].(string)
36 | if !ok || localDestPath == "" {
37 | // Default: use the name of the source file in current directory
38 | localDestPath = filepath.Base(containerSrcPath)
39 | }
40 |
41 | // Clean and create the destination directory if it doesn't exist
42 | localDestPath = filepath.Clean(localDestPath)
43 | if err := os.MkdirAll(filepath.Dir(localDestPath), 0755); err != nil {
44 | return mcp.NewToolResultText(fmt.Sprintf("Error creating destination directory: %v", err)), nil
45 | }
46 |
47 | // Copy the file from the container
48 | if err := copyFileFromContainer(ctx, containerID, containerSrcPath, localDestPath); err != nil {
49 | return mcp.NewToolResultText(fmt.Sprintf("Error copying file from container: %v", err)), nil
50 | }
51 |
52 | return mcp.NewToolResultText(fmt.Sprintf("Successfully copied %s from container %s to %s", containerSrcPath, containerID, localDestPath)), nil
53 | }
54 |
55 | // copyFileFromContainer copies a single file from the container to the local filesystem
56 | func copyFileFromContainer(ctx context.Context, containerID string, srcPath string, destPath string) error {
57 | cli, err := client.NewClientWithOpts(
58 | client.FromEnv,
59 | client.WithAPIVersionNegotiation(),
60 | )
61 | if err != nil {
62 | return fmt.Errorf("failed to create Docker client: %w", err)
63 | }
64 | defer cli.Close()
65 |
66 | // Create reader for the file from container
67 | reader, stat, err := cli.CopyFromContainer(ctx, containerID, srcPath)
68 | if err != nil {
69 | return fmt.Errorf("failed to copy from container: %w", err)
70 | }
71 | defer reader.Close()
72 |
73 | // Check if the source is a directory
74 | if stat.Mode.IsDir() {
75 | return fmt.Errorf("source path is a directory, only files are supported")
76 | }
77 |
78 | // Create tar reader since Docker sends files in tar format
79 | tr := tar.NewReader(reader)
80 |
81 | // Read the first (and should be only) file from the archive
82 | header, err := tr.Next()
83 | if err != nil {
84 | return fmt.Errorf("failed to read tar header: %w", err)
85 | }
86 |
87 | // Verify it's a regular file
88 | if header.Typeflag != tar.TypeReg {
89 | return fmt.Errorf("source is not a regular file")
90 | }
91 |
92 | // Create the destination file
93 | destFile, err := os.Create(destPath)
94 | if err != nil {
95 | return fmt.Errorf("failed to create destination file: %w", err)
96 | }
97 | defer destFile.Close()
98 |
99 | // Copy the content
100 | _, err = io.Copy(destFile, tr)
101 | if err != nil {
102 | return fmt.Errorf("failed to write file content: %w", err)
103 | }
104 |
105 | // Set file permissions from tar header
106 | if err := os.Chmod(destPath, os.FileMode(header.Mode)); err != nil {
107 | return fmt.Errorf("failed to set file permissions: %w", err)
108 | }
109 |
110 | return nil
111 | }
112 |
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/tools/exec.go:
--------------------------------------------------------------------------------
```go
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/docker/docker/api/types/container"
9 | "github.com/mark3labs/mcp-go/mcp"
10 | "github.com/docker/docker/client"
11 | "github.com/docker/docker/pkg/stdcopy"
12 | )
13 |
14 | // Exec executes commands in a container
15 | func Exec(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
16 | // Extract parameters
17 | containerID, ok := request.Params.Arguments["container_id"].(string)
18 | if !ok || containerID == "" {
19 | return mcp.NewToolResultText("container_id is required"), nil
20 | }
21 |
22 | // Commands can be a single string or an array of strings
23 | var commands []string
24 | if cmdsArr, ok := request.Params.Arguments["commands"].([]interface{}); ok {
25 | // It's an array of commands
26 | for _, cmd := range cmdsArr {
27 | if cmdStr, ok := cmd.(string); ok {
28 | commands = append(commands, cmdStr)
29 | } else {
30 | return mcp.NewToolResultText("Each command must be a string"), nil
31 | }
32 | }
33 | } else if cmdStr, ok := request.Params.Arguments["commands"].(string); ok {
34 | // It's a single command string
35 | commands = []string{cmdStr}
36 | } else {
37 | return mcp.NewToolResultText("commands must be a string or an array of strings"), nil
38 | }
39 |
40 | if len(commands) == 0 {
41 | return mcp.NewToolResultText("at least one command is required"), nil
42 | }
43 |
44 | // Execute each command and collect output
45 | var outputBuilder strings.Builder
46 | for i, cmd := range commands {
47 | // Format the command nicely in the output
48 | if i > 0 {
49 | outputBuilder.WriteString("\n\n")
50 | }
51 | outputBuilder.WriteString(fmt.Sprintf("$ %s\n", cmd))
52 |
53 | // Execute the command
54 | stdout, stderr, exitCode, err := executeCommandWithOutput(ctx, containerID, cmd)
55 | if err != nil {
56 | return mcp.NewToolResultText(fmt.Sprintf("Error executing command: %v", err)), nil
57 | }
58 |
59 | // Add the command output to the collector
60 | if stdout != "" {
61 | outputBuilder.WriteString(stdout)
62 | if !strings.HasSuffix(stdout, "\n") {
63 | outputBuilder.WriteString("\n")
64 | }
65 | }
66 | if stderr != "" {
67 | outputBuilder.WriteString("Error: ")
68 | outputBuilder.WriteString(stderr)
69 | if !strings.HasSuffix(stderr, "\n") {
70 | outputBuilder.WriteString("\n")
71 | }
72 | }
73 |
74 | // If the command failed, add the exit code and stop processing subsequent commands
75 | if exitCode != 0 {
76 | outputBuilder.WriteString(fmt.Sprintf("Command exited with code %d\n", exitCode))
77 | break
78 | }
79 | }
80 |
81 | return mcp.NewToolResultText(outputBuilder.String()), nil
82 | }
83 |
84 | // executeCommandWithOutput runs a command in a container and returns its stdout, stderr, exit code, and any error
85 | func executeCommandWithOutput(ctx context.Context, containerID string, cmd string) (stdout string, stderr string, exitCode int, err error) {
86 | cli, err := client.NewClientWithOpts(
87 | client.FromEnv,
88 | client.WithAPIVersionNegotiation(),
89 | )
90 | if err != nil {
91 | return "", "", -1, fmt.Errorf("failed to create Docker client: %w", err)
92 | }
93 | defer cli.Close()
94 |
95 | // Create the exec configuration
96 | exec, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
97 | Cmd: []string{"sh", "-c", cmd},
98 | AttachStdout: true,
99 | AttachStderr: true,
100 | })
101 | if err != nil {
102 | return "", "", -1, fmt.Errorf("failed to create exec: %w", err)
103 | }
104 |
105 | // Attach to the exec instance to get output
106 | resp, err := cli.ContainerExecAttach(ctx, exec.ID, container.ExecAttachOptions{})
107 | if err != nil {
108 | return "", "", -1, fmt.Errorf("failed to attach to exec: %w", err)
109 | }
110 | defer resp.Close()
111 |
112 | // Read the output
113 | var stdoutBuf, stderrBuf strings.Builder
114 | _, err = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, resp.Reader)
115 | if err != nil {
116 | return "", "", -1, fmt.Errorf("failed to read command output: %w", err)
117 | }
118 |
119 | // Get the exit code
120 | inspect, err := cli.ContainerExecInspect(ctx, exec.ID)
121 | if err != nil {
122 | return "", "", -1, fmt.Errorf("failed to inspect exec: %w", err)
123 | }
124 |
125 | return stdoutBuf.String(), stderrBuf.String(), inspect.ExitCode, nil
126 | }
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/tools/write-file.go:
--------------------------------------------------------------------------------
```go
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "path/filepath"
8 | "strings"
9 | "time"
10 |
11 | "github.com/docker/docker/api/types/container"
12 | "github.com/docker/docker/client"
13 | "github.com/mark3labs/mcp-go/mcp"
14 | )
15 |
16 | // WriteFile writes a file to the container's filesystem
17 | func WriteFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
18 | // Extract parameters
19 | containerID, ok := request.Params.Arguments["container_id"].(string)
20 | if !ok || containerID == "" {
21 | return mcp.NewToolResultText("container_id is required"), nil
22 | }
23 |
24 | fileName, ok := request.Params.Arguments["file_name"].(string)
25 | if !ok || fileName == "" {
26 | return mcp.NewToolResultText("file_name is required"), nil
27 | }
28 |
29 | fileContents, ok := request.Params.Arguments["file_contents"].(string)
30 | if !ok {
31 | return mcp.NewToolResultText("file_contents is required"), nil
32 | }
33 |
34 | // Get the destination path (optional parameter)
35 | destDir, ok := request.Params.Arguments["dest_dir"].(string)
36 | if !ok || destDir == "" {
37 | // Default: write to the working directory
38 | destDir = "/app"
39 | } else {
40 | // If provided but doesn't start with /, prepend /app/
41 | if !strings.HasPrefix(destDir, "/") {
42 | destDir = filepath.Join("/app", destDir)
43 | }
44 | }
45 |
46 | // Full path to the file
47 | fullPath := filepath.Join(destDir, fileName)
48 |
49 | // Create the directory if it doesn't exist
50 | if err := ensureDirectoryExists(ctx, containerID, destDir); err != nil {
51 | return mcp.NewToolResultText(fmt.Sprintf("Error creating directory: %v", err)), nil
52 | }
53 |
54 | // Write the file
55 | if err := writeFileToContainer(ctx, containerID, fullPath, fileContents); err != nil {
56 | return mcp.NewToolResultText(fmt.Sprintf("Error writing file: %v", err)), nil
57 | }
58 |
59 | return mcp.NewToolResultText(fmt.Sprintf("Successfully wrote file %s to container %s", fullPath, containerID)), nil
60 | }
61 |
62 | // ensureDirectoryExists creates a directory in the container if it doesn't already exist
63 | func ensureDirectoryExists(ctx context.Context, containerID, dirPath string) error {
64 | cli, err := client.NewClientWithOpts(
65 | client.FromEnv,
66 | client.WithAPIVersionNegotiation(),
67 | )
68 | if err != nil {
69 | return fmt.Errorf("failed to create Docker client: %w", err)
70 | }
71 | defer cli.Close()
72 |
73 | // Create the directory if it doesn't exist
74 | cmd := []string{"mkdir", "-p", dirPath}
75 | exec, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
76 | Cmd: cmd,
77 | })
78 | if err != nil {
79 | return fmt.Errorf("failed to create exec for mkdir: %w", err)
80 | }
81 |
82 | if err := cli.ContainerExecStart(ctx, exec.ID, container.ExecStartOptions{}); err != nil {
83 | return fmt.Errorf("failed to start exec for mkdir: %w", err)
84 | }
85 |
86 | return nil
87 | }
88 |
89 | // writeFileToContainer writes file contents to a file in the container
90 | func writeFileToContainer(ctx context.Context, containerID, filePath, contents string) error {
91 | cli, err := client.NewClientWithOpts(
92 | client.FromEnv,
93 | client.WithAPIVersionNegotiation(),
94 | )
95 | if err != nil {
96 | return fmt.Errorf("failed to create Docker client: %w", err)
97 | }
98 | defer cli.Close()
99 |
100 | // Command to write the content to the specified file using cat
101 | cmd := []string{"sh", "-c", fmt.Sprintf("cat > %s", filePath)}
102 |
103 | // Create the exec configuration
104 | execConfig := container.ExecOptions{
105 | Cmd: cmd,
106 | AttachStdin: true,
107 | AttachStdout: true,
108 | AttachStderr: true,
109 | }
110 |
111 | // Create the exec instance
112 | execIDResp, err := cli.ContainerExecCreate(ctx, containerID, execConfig)
113 | if err != nil {
114 | return fmt.Errorf("failed to create exec: %w", err)
115 | }
116 |
117 | // Attach to the exec instance
118 | resp, err := cli.ContainerExecAttach(ctx, execIDResp.ID, container.ExecAttachOptions{})
119 | if err != nil {
120 | return fmt.Errorf("failed to attach to exec: %w", err)
121 | }
122 | defer resp.Close()
123 |
124 | // Write the content to the container's stdin
125 | _, err = io.Copy(resp.Conn, strings.NewReader(contents))
126 | if err != nil {
127 | return fmt.Errorf("failed to write content to container: %w", err)
128 | }
129 | resp.CloseWrite()
130 |
131 | // Wait for the command to complete
132 | for {
133 | inspect, err := cli.ContainerExecInspect(ctx, execIDResp.ID)
134 | if err != nil {
135 | return fmt.Errorf("failed to inspect exec: %w", err)
136 | }
137 | if !inspect.Running {
138 | if inspect.ExitCode != 0 {
139 | return fmt.Errorf("command exited with code %d", inspect.ExitCode)
140 | }
141 | break
142 | }
143 | // Small sleep to avoid hammering the Docker API
144 | time.Sleep(100 * time.Millisecond)
145 | }
146 |
147 | return nil
148 | }
149 |
```
--------------------------------------------------------------------------------
/install.ps1:
--------------------------------------------------------------------------------
```
1 | # Function to check if running in a terminal that supports colors
2 | function Test-ColorSupport {
3 | # Check if we're in a terminal that supports VirtualTerminalLevel
4 | $supportsVT = $false
5 | try {
6 | $supportsVT = [Console]::IsOutputRedirected -eq $false -and
7 | [Console]::IsErrorRedirected -eq $false -and
8 | [Environment]::GetEnvironmentVariable("TERM") -ne $null
9 | } catch {
10 | $supportsVT = $false
11 | }
12 | return $supportsVT
13 | }
14 |
15 | # Function to write colored output
16 | function Write-ColoredMessage {
17 | param(
18 | [string]$Message,
19 | [System.ConsoleColor]$Color = [System.ConsoleColor]::White
20 | )
21 |
22 | if (Test-ColorSupport) {
23 | $originalColor = [Console]::ForegroundColor
24 | [Console]::ForegroundColor = $Color
25 | Write-Host $Message
26 | [Console]::ForegroundColor = $originalColor
27 | } else {
28 | Write-Host $Message
29 | }
30 | }
31 |
32 | # Function to stop running instances
33 | function Stop-RunningInstances {
34 | param(
35 | [string]$ProcessName
36 | )
37 |
38 | try {
39 | $processes = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue
40 | if ($processes) {
41 | $processes | ForEach-Object {
42 | try {
43 | $_.Kill()
44 | $_.WaitForExit(1000)
45 | } catch {
46 | # Ignore errors if process already exited
47 | }
48 | }
49 | Start-Sleep -Seconds 1 # Give processes time to fully exit
50 | }
51 | } catch {
52 | # Ignore errors if no processes found
53 | }
54 | }
55 |
56 | # Check if Docker is installed
57 | if (-not (Get-Command "docker" -ErrorAction SilentlyContinue)) {
58 | Write-ColoredMessage "Error: Docker is not installed" -Color Red
59 | Write-ColoredMessage "Please install Docker Desktop for Windows:" -Color Yellow
60 | Write-Host " https://docs.docker.com/desktop/install/windows-install/"
61 | exit 1
62 | }
63 |
64 | # Check if Docker daemon is running
65 | try {
66 | docker info | Out-Null
67 | } catch {
68 | Write-ColoredMessage "Error: Docker daemon is not running" -Color Red
69 | Write-ColoredMessage "Please start Docker Desktop and try again" -Color Yellow
70 | exit 1
71 | }
72 |
73 | Write-ColoredMessage "Downloading latest release..." -Color Green
74 |
75 | # Determine architecture
76 | $arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "386" }
77 |
78 | # Get the latest release URL
79 | try {
80 | $apiResponse = Invoke-RestMethod -Uri "https://api.github.com/repos/Automata-Labs-team/code-sandbox-mcp/releases/latest"
81 | $asset = $apiResponse.assets | Where-Object { $_.name -like "code-sandbox-mcp-windows-$arch.exe" }
82 | } catch {
83 | Write-ColoredMessage "Error: Failed to fetch latest release information" -Color Red
84 | Write-Host $_.Exception.Message
85 | exit 1
86 | }
87 |
88 | if (-not $asset) {
89 | Write-ColoredMessage "Error: Could not find release for windows-$arch" -Color Red
90 | exit 1
91 | }
92 |
93 | # Create installation directory
94 | $installDir = "$env:LOCALAPPDATA\code-sandbox-mcp"
95 | New-Item -ItemType Directory -Force -Path $installDir | Out-Null
96 |
97 | # Download to a temporary file first
98 | $tempFile = "$installDir\code-sandbox-mcp.tmp"
99 | Write-ColoredMessage "Installing to $installDir\code-sandbox-mcp.exe..." -Color Green
100 |
101 | try {
102 | # Download the binary to temporary file
103 | Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $tempFile
104 |
105 | # Stop any running instances
106 | Stop-RunningInstances -ProcessName "code-sandbox-mcp"
107 |
108 | # Try to move the temporary file to the final location
109 | try {
110 | Move-Item -Path $tempFile -Destination "$installDir\code-sandbox-mcp.exe" -Force
111 | } catch {
112 | Write-ColoredMessage "Error: Failed to install the binary. Please ensure no instances are running and try again." -Color Red
113 | Remove-Item -Path $tempFile -ErrorAction SilentlyContinue
114 | exit 1
115 | }
116 | } catch {
117 | Write-ColoredMessage "Error: Failed to download or install the binary" -Color Red
118 | Write-Host $_.Exception.Message
119 | Remove-Item -Path $tempFile -ErrorAction SilentlyContinue
120 | exit 1
121 | }
122 |
123 | # Add to Claude Desktop config
124 | Write-ColoredMessage "Adding to Claude Desktop configuration..." -Color Green
125 | try {
126 | & "$installDir\code-sandbox-mcp.exe" --install
127 | } catch {
128 | Write-ColoredMessage "Error: Failed to configure Claude Desktop" -Color Red
129 | Write-Host $_.Exception.Message
130 | exit 1
131 | }
132 |
133 | Write-ColoredMessage "Installation complete!" -Color Green
134 | Write-Host "You can now use code-sandbox-mcp with Claude Desktop or other AI applications."
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/tools/copy-file.go:
--------------------------------------------------------------------------------
```go
1 | package tools
2 |
3 | import (
4 | "archive/tar"
5 | "bytes"
6 | "context"
7 | "fmt"
8 | "io"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 |
13 | "github.com/docker/docker/api/types/container"
14 | "github.com/docker/docker/client"
15 | "github.com/mark3labs/mcp-go/mcp"
16 | )
17 |
18 | // CopyFile copies a single local file to a container's filesystem
19 | func CopyFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
20 | // Extract parameters
21 | containerID, ok := request.Params.Arguments["container_id"].(string)
22 | if !ok || containerID == "" {
23 | return mcp.NewToolResultText("container_id is required"), nil
24 | }
25 |
26 | localSrcFile, ok := request.Params.Arguments["local_src_file"].(string)
27 | if !ok || localSrcFile == "" {
28 | return mcp.NewToolResultText("local_src_file is required"), nil
29 | }
30 |
31 | // Clean and validate the source path
32 | localSrcFile = filepath.Clean(localSrcFile)
33 | info, err := os.Stat(localSrcFile)
34 | if err != nil {
35 | return mcp.NewToolResultText(fmt.Sprintf("Error accessing source file: %v", err)), nil
36 | }
37 |
38 | if info.IsDir() {
39 | return mcp.NewToolResultText("local_src_file must be a file, not a directory"), nil
40 | }
41 |
42 | // Get the destination path (optional parameter)
43 | destPath, ok := request.Params.Arguments["dest_path"].(string)
44 | if !ok || destPath == "" {
45 | // Default: use the name of the source file
46 | destPath = filepath.Join("/app", filepath.Base(localSrcFile))
47 | } else {
48 | // If provided but doesn't start with /, prepend /app/
49 | if !strings.HasPrefix(destPath, "/") {
50 | destPath = filepath.Join("/app", destPath)
51 | }
52 | }
53 |
54 | // Create destination directory in container if it doesn't exist
55 | destDir := filepath.Dir(destPath)
56 | if err := createDirectoryInContainer(ctx, containerID, destDir); err != nil {
57 | return mcp.NewToolResultText(fmt.Sprintf("Error creating destination directory: %v", err)), nil
58 | }
59 |
60 | // Copy the file to the container
61 | if err := copyFileToContainer(ctx, containerID, localSrcFile, destPath); err != nil {
62 | return mcp.NewToolResultText(fmt.Sprintf("Error copying file to container: %v", err)), nil
63 | }
64 |
65 | return mcp.NewToolResultText(fmt.Sprintf("Successfully copied %s to %s in container %s", localSrcFile, destPath, containerID)), nil
66 | }
67 |
68 | // createDirectoryInContainer creates a directory in the container if it doesn't exist
69 | func createDirectoryInContainer(ctx context.Context, containerID string, dirPath string) error {
70 | cli, err := client.NewClientWithOpts(
71 | client.FromEnv,
72 | client.WithAPIVersionNegotiation(),
73 | )
74 | if err != nil {
75 | return fmt.Errorf("failed to create Docker client: %w", err)
76 | }
77 | defer cli.Close()
78 |
79 | createDirCmd := []string{"mkdir", "-p", dirPath}
80 | exec, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
81 | Cmd: createDirCmd,
82 | AttachStdout: true,
83 | AttachStderr: true,
84 | })
85 | if err != nil {
86 | return fmt.Errorf("failed to create exec: %w", err)
87 | }
88 |
89 | if err := cli.ContainerExecStart(ctx, exec.ID, container.ExecStartOptions{}); err != nil {
90 | return fmt.Errorf("failed to start exec: %w", err)
91 | }
92 |
93 | return nil
94 | }
95 |
96 | // copyFileToContainer copies a single file to the container
97 | func copyFileToContainer(ctx context.Context, containerID string, srcPath string, destPath string) error {
98 | cli, err := client.NewClientWithOpts(
99 | client.FromEnv,
100 | client.WithAPIVersionNegotiation(),
101 | )
102 | if err != nil {
103 | return fmt.Errorf("failed to create Docker client: %w", err)
104 | }
105 | defer cli.Close()
106 |
107 | // Open and stat the source file
108 | srcFile, err := os.Open(srcPath)
109 | if err != nil {
110 | return fmt.Errorf("failed to open source file: %w", err)
111 | }
112 | defer srcFile.Close()
113 |
114 | srcInfo, err := srcFile.Stat()
115 | if err != nil {
116 | return fmt.Errorf("failed to stat source file: %w", err)
117 | }
118 |
119 | // Create a buffer to write our archive to
120 | var buf bytes.Buffer
121 |
122 | // Create a new tar archive
123 | tw := tar.NewWriter(&buf)
124 |
125 | // Create tar header
126 | header := &tar.Header{
127 | Name: filepath.Base(destPath),
128 | Size: srcInfo.Size(),
129 | Mode: int64(srcInfo.Mode()),
130 | ModTime: srcInfo.ModTime(),
131 | }
132 |
133 | // Write header
134 | if err := tw.WriteHeader(header); err != nil {
135 | return fmt.Errorf("failed to write tar header: %w", err)
136 | }
137 |
138 | // Copy file content to tar archive
139 | if _, err := io.Copy(tw, srcFile); err != nil {
140 | return fmt.Errorf("failed to write file content to tar: %w", err)
141 | }
142 |
143 | // Close tar writer
144 | if err := tw.Close(); err != nil {
145 | return fmt.Errorf("failed to close tar writer: %w", err)
146 | }
147 |
148 | // Copy the tar archive to the container
149 | err = cli.CopyToContainer(ctx, containerID, filepath.Dir(destPath), &buf, container.CopyToContainerOptions{})
150 | if err != nil {
151 | return fmt.Errorf("failed to copy to container: %w", err)
152 | }
153 |
154 | return nil
155 | }
156 |
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/tools/copy-project.go:
--------------------------------------------------------------------------------
```go
1 | package tools
2 |
3 | import (
4 | "archive/tar"
5 | "bytes"
6 | "context"
7 | "fmt"
8 | "io"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 | "time"
13 |
14 | "github.com/docker/docker/api/types/container"
15 | "github.com/docker/docker/client"
16 | "github.com/mark3labs/mcp-go/mcp"
17 | )
18 |
19 | // CopyProject copies a local directory to a container's filesystem
20 | func CopyProject(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
21 | // Extract parameters
22 | containerID, ok := request.Params.Arguments["container_id"].(string)
23 | if !ok || containerID == "" {
24 | return mcp.NewToolResultText("container_id is required"), nil
25 | }
26 |
27 | localSrcDir, ok := request.Params.Arguments["local_src_dir"].(string)
28 | if !ok || localSrcDir == "" {
29 | return mcp.NewToolResultText("local_src_dir is required"), nil
30 | }
31 |
32 | // Clean and validate the source path
33 | localSrcDir = filepath.Clean(localSrcDir)
34 | info, err := os.Stat(localSrcDir)
35 | if err != nil {
36 | return mcp.NewToolResultText(fmt.Sprintf("Error accessing source directory: %v", err)), nil
37 | }
38 |
39 | if !info.IsDir() {
40 | return mcp.NewToolResultText("local_src_dir must be a directory"), nil
41 | }
42 |
43 | // Get the destination path (optional parameter)
44 | destDir, ok := request.Params.Arguments["dest_dir"].(string)
45 | if !ok || destDir == "" {
46 | // Default: use the name of the source directory
47 | destDir = filepath.Join("/app", filepath.Base(localSrcDir))
48 | } else {
49 | // If provided but doesn't start with /, prepend /app/
50 | if !strings.HasPrefix(destDir, "/") {
51 | destDir = filepath.Join("/app", destDir)
52 | }
53 | }
54 |
55 | // Create tar archive of the source directory
56 | tarBuffer, err := createTarArchive(localSrcDir)
57 | if err != nil {
58 | return mcp.NewToolResultText(fmt.Sprintf("Error creating tar archive: %v", err)), nil
59 | }
60 |
61 | // Create a temporary file name for the tar archive in the container
62 | tarFileName := filepath.Join("/tmp", fmt.Sprintf("project_%s.tar", filepath.Base(localSrcDir)))
63 |
64 | // Copy the tar archive to the container's temp directory
65 | err = copyToContainer(ctx, containerID, "/tmp", tarBuffer)
66 | if err != nil {
67 | return mcp.NewToolResultText(fmt.Sprintf("Error copying to container: %v", err)), nil
68 | }
69 |
70 | // Extract the tar archive in the container
71 | err = extractTarInContainer(ctx, containerID, tarFileName, destDir)
72 | if err != nil {
73 | return mcp.NewToolResultText(fmt.Sprintf("Error extracting archive in container: %v", err)), nil
74 | }
75 |
76 | // Clean up the temporary tar file
77 | cleanupCmd := []string{"rm", tarFileName}
78 | if err := executeCommand(ctx, containerID, cleanupCmd); err != nil {
79 | // Just log the error but don't fail the operation
80 | fmt.Printf("Warning: Failed to clean up temporary tar file: %v\n", err)
81 | }
82 |
83 | return mcp.NewToolResultText(fmt.Sprintf("Successfully copied %s to %s in container %s", localSrcDir, destDir, containerID)), nil
84 | }
85 |
86 | // createTarArchive creates a tar archive of the specified source path
87 | func createTarArchive(srcPath string) (io.Reader, error) {
88 | buf := new(bytes.Buffer)
89 | tw := tar.NewWriter(buf)
90 | defer tw.Close()
91 |
92 | srcPath = filepath.Clean(srcPath)
93 | baseDir := filepath.Base(srcPath)
94 |
95 | err := filepath.Walk(srcPath, func(file string, fi os.FileInfo, err error) error {
96 | if err != nil {
97 | return err
98 | }
99 |
100 | // Create tar header
101 | header, err := tar.FileInfoHeader(fi, fi.Name())
102 | if err != nil {
103 | return err
104 | }
105 |
106 | // Maintain directory structure relative to the source directory
107 | relPath, err := filepath.Rel(srcPath, file)
108 | if err != nil {
109 | return err
110 | }
111 |
112 | if relPath == "." {
113 | // Skip the root directory itself
114 | return nil
115 | }
116 |
117 | header.Name = filepath.Join(baseDir, relPath)
118 |
119 | if err := tw.WriteHeader(header); err != nil {
120 | return err
121 | }
122 |
123 | // If it's a regular file, write its content
124 | if fi.Mode().IsRegular() {
125 | f, err := os.Open(file)
126 | if err != nil {
127 | return err
128 | }
129 | defer f.Close()
130 |
131 | if _, err := io.Copy(tw, f); err != nil {
132 | return err
133 | }
134 | }
135 | return nil
136 | })
137 |
138 | if err != nil {
139 | return nil, err
140 | }
141 |
142 | return buf, nil
143 | }
144 |
145 | // copyToContainer copies a tar archive to a container
146 | func copyToContainer(ctx context.Context, containerID string, destPath string, tarArchive io.Reader) error {
147 | cli, err := client.NewClientWithOpts(
148 | client.FromEnv,
149 | client.WithAPIVersionNegotiation(),
150 | )
151 | if err != nil {
152 | return fmt.Errorf("failed to create Docker client: %w", err)
153 | }
154 | defer cli.Close()
155 |
156 | // Make sure the container exists and is running
157 | _, err = cli.ContainerInspect(ctx, containerID)
158 | if err != nil {
159 | return fmt.Errorf("failed to inspect container: %w", err)
160 | }
161 |
162 | // Create the destination directory in the container if it doesn't exist
163 | createDirCmd := []string{"mkdir", "-p", destPath}
164 | if err := executeCommand(ctx, containerID, createDirCmd); err != nil {
165 | return fmt.Errorf("failed to create destination directory: %w", err)
166 | }
167 |
168 | // Copy the tar archive to the container
169 | err = cli.CopyToContainer(ctx, containerID, destPath, tarArchive, container.CopyToContainerOptions{})
170 | if err != nil {
171 | return fmt.Errorf("failed to copy to container: %w", err)
172 | }
173 |
174 | return nil
175 | }
176 |
177 | // extractTarInContainer extracts a tar archive inside the container
178 | func extractTarInContainer(ctx context.Context, containerID string, tarFilePath string, destPath string) error {
179 | // Create the destination directory if it doesn't exist
180 | mkdirCmd := []string{"mkdir", "-p", destPath}
181 | if err := executeCommand(ctx, containerID, mkdirCmd); err != nil {
182 | return fmt.Errorf("failed to create destination directory: %w", err)
183 | }
184 |
185 | // Extract the tar archive
186 | extractCmd := []string{"tar", "-xf", tarFilePath, "-C", destPath}
187 | if err := executeCommand(ctx, containerID, extractCmd); err != nil {
188 | return fmt.Errorf("failed to extract tar archive: %w", err)
189 | }
190 |
191 | return nil
192 | }
193 |
194 | // executeCommand runs a command in a container and waits for it to complete
195 | func executeCommand(ctx context.Context, containerID string, cmd []string) error {
196 | cli, err := client.NewClientWithOpts(
197 | client.FromEnv,
198 | client.WithAPIVersionNegotiation(),
199 | )
200 | if err != nil {
201 | return fmt.Errorf("failed to create Docker client: %w", err)
202 | }
203 | defer cli.Close()
204 |
205 | // Create the exec configuration
206 | exec, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
207 | Cmd: cmd,
208 | AttachStdout: true,
209 | AttachStderr: true,
210 | })
211 | if err != nil {
212 | return fmt.Errorf("failed to create exec: %w", err)
213 | }
214 |
215 | // Start the exec command
216 | if err := cli.ContainerExecStart(ctx, exec.ID, container.ExecStartOptions{}); err != nil {
217 | return fmt.Errorf("failed to start exec: %w", err)
218 | }
219 |
220 | // Wait for the command to complete
221 | for {
222 | inspect, err := cli.ContainerExecInspect(ctx, exec.ID)
223 | if err != nil {
224 | return fmt.Errorf("failed to inspect exec: %w", err)
225 | }
226 | if !inspect.Running {
227 | if inspect.ExitCode != 0 {
228 | return fmt.Errorf("command exited with code %d", inspect.ExitCode)
229 | }
230 | break
231 | }
232 | // Small sleep to avoid hammering the Docker API
233 | time.Sleep(100 * time.Millisecond)
234 | }
235 |
236 | return nil
237 | }
238 |
```
--------------------------------------------------------------------------------
/src/code-sandbox-mcp/main.go:
--------------------------------------------------------------------------------
```go
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "os"
9 |
10 | "github.com/Automata-Labs-team/code-sandbox-mcp/installer"
11 | "github.com/Automata-Labs-team/code-sandbox-mcp/resources"
12 | "github.com/Automata-Labs-team/code-sandbox-mcp/tools"
13 | "github.com/mark3labs/mcp-go/mcp"
14 | "github.com/mark3labs/mcp-go/server"
15 | )
16 |
17 | func init() {
18 | // Check for --install flag
19 | installFlag := flag.Bool("install", false, "Add this binary to Claude Desktop config")
20 | noUpdateFlag := flag.Bool("no-update", false, "Disable auto-update check")
21 | flag.Parse()
22 |
23 | if *installFlag {
24 | if err := installer.InstallConfig(); err != nil {
25 | fmt.Fprintf(os.Stderr, "Error: %v\n", err)
26 | os.Exit(1)
27 | }
28 | os.Exit(0)
29 | }
30 |
31 | // Check for updates unless disabled
32 | if !*noUpdateFlag {
33 | if hasUpdate, downloadURL, err := installer.CheckForUpdate(); err != nil {
34 | fmt.Fprintf(os.Stderr, "Warning: Failed to check for updates: %v\n", err)
35 | os.Exit(1)
36 | } else if hasUpdate {
37 | fmt.Println("Updating to new version...")
38 | if err := installer.PerformUpdate(downloadURL); err != nil {
39 | fmt.Fprintf(os.Stderr, "Warning: Failed to update: %v\n", err)
40 | }
41 | fmt.Println("Update complete. Restarting...")
42 | }
43 | }
44 | }
45 |
46 | func main() {
47 | port := flag.String("port", "9520", "Port to listen on")
48 | transport := flag.String("transport", "stdio", "Transport to use (stdio, sse)")
49 | flag.Parse()
50 | s := server.NewMCPServer("code-sandbox-mcp", "v1.0.0", server.WithLogging(), server.WithResourceCapabilities(true, true), server.WithPromptCapabilities(false))
51 | s.AddNotificationHandler("notifications/error", handleNotification)
52 | // Register tools
53 | // Initialize a new compute environment for code execution
54 | initializeTool := mcp.NewTool("sandbox_initialize",
55 | mcp.WithDescription(
56 | "Initialize a new compute environment for code execution. \n"+
57 | "Creates a container based on the specified Docker image or defaults to a slim debian image with Python. \n"+
58 | "Returns a container_id that can be used with other tools to interact with this environment.",
59 | ),
60 | mcp.WithString("image",
61 | mcp.Description("Docker image to use as the base environment (e.g., 'python:3.12-slim-bookworm')"),
62 | mcp.DefaultString("python:3.12-slim-bookworm"),
63 | ),
64 | )
65 |
66 | // Copy a directory to the sandboxed filesystem
67 | copyProjectTool := mcp.NewTool("copy_project",
68 | mcp.WithDescription(
69 | "Copy a directory to the sandboxed filesystem. \n"+
70 | "Transfers a local directory and its contents to the specified container.",
71 | ),
72 | mcp.WithString("container_id",
73 | mcp.Required(),
74 | mcp.Description("ID of the container returned from the initialize call"),
75 | ),
76 | mcp.WithString("local_src_dir",
77 | mcp.Required(),
78 | mcp.Description("Path to a directory in the local file system"),
79 | ),
80 | mcp.WithString("dest_dir",
81 | mcp.Description("Path to save the src directory in the sandbox environment, relative to the container working dir"),
82 | ),
83 | )
84 |
85 | // Write a file to the sandboxed filesystem
86 | writeFileTool := mcp.NewTool("write_file_sandbox",
87 | mcp.WithDescription(
88 | "Write a file to the sandboxed filesystem. \n"+
89 | "Creates a file with the specified content in the container.",
90 | ),
91 | mcp.WithString("container_id",
92 | mcp.Required(),
93 | mcp.Description("ID of the container returned from the initialize call"),
94 | ),
95 | mcp.WithString("file_name",
96 | mcp.Required(),
97 | mcp.Description("Name of the file to create"),
98 | ),
99 | mcp.WithString("file_contents",
100 | mcp.Required(),
101 | mcp.Description("Contents to write to the file"),
102 | ),
103 | mcp.WithString("dest_dir",
104 | mcp.Description("Directory to create the file in, relative to the container working dir"),
105 | mcp.Description("Default: ${WORKDIR}"),
106 | ),
107 | )
108 |
109 | // Execute commands in the sandboxed environment
110 | execTool := mcp.NewTool("sandbox_exec",
111 | mcp.WithDescription(
112 | "Execute commands in the sandboxed environment. \n"+
113 | "Runs one or more shell commands in the specified container and returns the output.",
114 | ),
115 | mcp.WithString("container_id",
116 | mcp.Required(),
117 | mcp.Description("ID of the container returned from the initialize call"),
118 | ),
119 | mcp.WithArray("commands",
120 | mcp.Required(),
121 | mcp.Description("List of command(s) to run in the sandboxed environment"),
122 | mcp.Description("Example: [\"apt-get update\", \"pip install numpy\", \"python script.py\"]"),
123 | ),
124 | )
125 |
126 | // Copy a single file to the sandboxed filesystem
127 | copyFileTool := mcp.NewTool("copy_file",
128 | mcp.WithDescription(
129 | "Copy a single file to the sandboxed filesystem. \n"+
130 | "Transfers a local file to the specified container.",
131 | ),
132 | mcp.WithString("container_id",
133 | mcp.Required(),
134 | mcp.Description("ID of the container returned from the initialize call"),
135 | ),
136 | mcp.WithString("local_src_file",
137 | mcp.Required(),
138 | mcp.Description("Path to a file in the local file system"),
139 | ),
140 | mcp.WithString("dest_path",
141 | mcp.Description("Path to save the file in the sandbox environment, relative to the container working dir"),
142 | ),
143 | )
144 |
145 | // Copy a file from container to local filesystem
146 | copyFileFromContainerTool := mcp.NewTool("copy_file_from_sandbox",
147 | mcp.WithDescription(
148 | "Copy a single file from the sandboxed filesystem to the local filesystem. \n"+
149 | "Transfers a file from the specified container to the local system.",
150 | ),
151 | mcp.WithString("container_id",
152 | mcp.Required(),
153 | mcp.Description("ID of the container to copy from"),
154 | ),
155 | mcp.WithString("container_src_path",
156 | mcp.Required(),
157 | mcp.Description("Path to the file in the container to copy"),
158 | ),
159 | mcp.WithString("local_dest_path",
160 | mcp.Description("Path where to save the file in the local filesystem"),
161 | mcp.Description("Default: Current directory with the same filename"),
162 | ),
163 | )
164 |
165 | // Stop and remove a container
166 | stopContainerTool := mcp.NewTool("sandbox_stop",
167 | mcp.WithDescription(
168 | "Stop and remove a running container sandbox. \n"+
169 | "Gracefully stops the specified container and removes it along with its volumes.",
170 | ),
171 | mcp.WithString("container_id",
172 | mcp.Required(),
173 | mcp.Description("ID of the container to stop and remove"),
174 | ),
175 | )
176 |
177 | // Register dynamic resource for container logs
178 | // Dynamic resource example - Container Logs by ID
179 | containerLogsTemplate := mcp.NewResourceTemplate(
180 | "containers://{id}/logs",
181 | "Container Logs",
182 | mcp.WithTemplateDescription("Returns all container logs from the specified container. Logs are returned as a single text resource."),
183 | mcp.WithTemplateMIMEType("text/plain"),
184 | mcp.WithTemplateAnnotations([]mcp.Role{mcp.RoleAssistant, mcp.RoleUser}, 0.5),
185 | )
186 |
187 | s.AddResourceTemplate(containerLogsTemplate, resources.GetContainerLogs)
188 | s.AddTool(initializeTool, tools.InitializeEnvironment)
189 | s.AddTool(copyProjectTool, tools.CopyProject)
190 | s.AddTool(writeFileTool, tools.WriteFile)
191 | s.AddTool(execTool, tools.Exec)
192 | s.AddTool(copyFileTool, tools.CopyFile)
193 | s.AddTool(copyFileFromContainerTool, tools.CopyFileFromContainer)
194 | s.AddTool(stopContainerTool, tools.StopContainer)
195 | switch *transport {
196 | case "stdio":
197 | if err := server.ServeStdio(s); err != nil {
198 | s.SendNotificationToClient(context.Background(), "notifications/error", map[string]interface{}{
199 | "message": fmt.Sprintf("Failed to start stdio server: %v", err),
200 | })
201 | }
202 | case "sse":
203 | sseServer := server.NewSSEServer(s)
204 | if err := sseServer.Start(fmt.Sprintf(":%s", *port)); err != nil {
205 | s.SendNotificationToClient(context.Background(), "notifications/error", map[string]interface{}{
206 | "message": fmt.Sprintf("Failed to start SSE server: %v", err),
207 | })
208 | }
209 | default:
210 | s.SendNotificationToClient(context.Background(), "notifications/error", map[string]interface{}{
211 | "message": fmt.Sprintf("Invalid transport: %s", *transport),
212 | })
213 | }
214 | }
215 |
216 | func handleNotification(
217 | ctx context.Context,
218 | notification mcp.JSONRPCNotification,
219 | ) {
220 | log.Printf("Received notification from client: %s", notification.Method)
221 | }
222 |
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | workflow_dispatch:
8 | inputs:
9 | version_increment:
10 | description: 'Version increment type'
11 | required: true
12 | default: 'patch'
13 | type: choice
14 | options:
15 | - patch
16 | - minor
17 | - major
18 | prerelease:
19 | description: 'Mark as prerelease'
20 | required: true
21 | default: false
22 | type: boolean
23 |
24 | jobs:
25 | release:
26 | runs-on: ubuntu-latest
27 | permissions:
28 | contents: write
29 | steps:
30 | - name: Checkout code
31 | uses: actions/checkout@v4
32 | with:
33 | fetch-depth: 0
34 |
35 | - name: Set up Go
36 | uses: actions/setup-go@v4
37 | with:
38 | go-version: '1.21'
39 | cache: true
40 |
41 | - name: Get version and generate changelog
42 | id: get_version
43 | run: |
44 | # Get the latest tag or use v0.0.0 if no tags exist
45 | LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
46 |
47 | # Remove 'v' prefix for version calculations
48 | VERSION=${LATEST_TAG#v}
49 | MAJOR=$(echo $VERSION | cut -d. -f1)
50 | MINOR=$(echo $VERSION | cut -d. -f2)
51 | PATCH=$(echo $VERSION | cut -d. -f3)
52 |
53 | # Handle version increment based on input or default to patch
54 | if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
55 | case "${{ inputs.version_increment }}" in
56 | "major")
57 | MAJOR=$((MAJOR + 1))
58 | MINOR=0
59 | PATCH=0
60 | ;;
61 | "minor")
62 | MINOR=$((MINOR + 1))
63 | PATCH=0
64 | ;;
65 | "patch")
66 | PATCH=$((PATCH + 1))
67 | ;;
68 | esac
69 | else
70 | # Auto increment patch version for push events
71 | PATCH=$((PATCH + 1))
72 | fi
73 |
74 | NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
75 | echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
76 |
77 | # Function to extract and format issue/PR references
78 | format_references() {
79 | local msg="$1"
80 | # Look for common issue/PR reference patterns (#123, GH-123, fixes #123, etc.)
81 | local refs=$(echo "$msg" | grep -o -E '(#[0-9]+|GH-[0-9]+)' || true)
82 | if [ ! -z "$refs" ]; then
83 | local formatted_refs=""
84 | while read -r ref; do
85 | # Remove any prefix and get just the number
86 | local num=$(echo "$ref" | grep -o '[0-9]\+')
87 | formatted_refs="$formatted_refs [${ref}](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/issues/${num})"
88 | done <<< "$refs"
89 | echo "$formatted_refs"
90 | fi
91 | }
92 |
93 | # Function to format commit messages by type
94 | format_commits() {
95 | local pattern=$1
96 | local title=$2
97 | # Include author, date, and full commit info
98 | local commits=$(git log --pretty=format:"- %s ([%h](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/%H)) - %an, %as" ${LATEST_TAG}..HEAD | grep -E "^$pattern" || true)
99 | if [ ! -z "$commits" ]; then
100 | echo "### $title"
101 | while IFS= read -r commit; do
102 | if [ ! -z "$commit" ]; then
103 | # Extract the commit message for issue/PR reference search
104 | local commit_msg=$(echo "$commit" | sed -E 's/^- ([^(]+).*/\1/')
105 | local refs=$(format_references "$commit_msg")
106 | if [ ! -z "$refs" ]; then
107 | # Add references to the end of the commit line
108 | echo "$commit - References: $refs"
109 | else
110 | echo "$commit"
111 | fi
112 | fi
113 | done <<< "$commits" | sed 's/^[^:]*: //'
114 | echo ""
115 | fi
116 | }
117 |
118 | # Generate categorized changelog
119 | if [ "$LATEST_TAG" != "v0.0.0" ]; then
120 | CHANGES=$(
121 | {
122 | echo "## 📋 Changelog"
123 | echo "$(git log -1 --pretty=format:"Generated on: %ad" --date=format:"%Y-%m-%d %H:%M:%S %Z")"
124 | echo ""
125 | format_commits "feat(\w*)?:" "🚀 New Features"
126 | format_commits "fix(\w*)?:" "🐛 Bug Fixes"
127 | format_commits "perf(\w*)?:" "⚡ Performance Improvements"
128 | format_commits "refactor(\w*)?:" "♻️ Refactoring"
129 | format_commits "test(\w*)?:" "🧪 Testing"
130 | format_commits "docs(\w*)?:" "📚 Documentation"
131 | format_commits "style(\w*)?:" "💎 Styling"
132 | format_commits "chore(\w*)?:" "🔧 Maintenance"
133 |
134 | # Get other commits that don't match conventional commit format
135 | echo "### 🔍 Other Changes"
136 | git log --pretty=format:"- %s ([%h](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/%H)) - %an, %as" ${LATEST_TAG}..HEAD | grep -vE "^(feat|fix|perf|refactor|test|docs|style|chore)(\w*)?:" | while IFS= read -r commit; do
137 | if [ ! -z "$commit" ]; then
138 | local commit_msg=$(echo "$commit" | sed -E 's/^- ([^(]+).*/\1/')
139 | local refs=$(format_references "$commit_msg")
140 | if [ ! -z "$refs" ]; then
141 | echo "$commit - References: $refs"
142 | else
143 | echo "$commit"
144 | fi
145 | fi
146 | done || true
147 | } | sed '/^$/d'
148 | )
149 | else
150 | # For first release, include all commits with metadata and links
151 | CHANGES=$(
152 | {
153 | echo "## 📋 Initial Release Changelog"
154 | echo "$(git log -1 --pretty=format:"Generated on: %ad" --date=format:"%Y-%m-%d %H:%M:%S %Z")"
155 | echo ""
156 | git log --pretty=format:"- %s ([%h](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/%H)) - %an, %as" | while IFS= read -r commit; do
157 | if [ ! -z "$commit" ]; then
158 | local commit_msg=$(echo "$commit" | sed -E 's/^- ([^(]+).*/\1/')
159 | local refs=$(format_references "$commit_msg")
160 | if [ ! -z "$refs" ]; then
161 | echo "$commit - References: $refs"
162 | else
163 | echo "$commit"
164 | fi
165 | fi
166 | done
167 | }
168 | )
169 | fi
170 |
171 | # Save changes to output
172 | echo "changes<<EOF" >> $GITHUB_OUTPUT
173 | echo "$CHANGES" >> $GITHUB_OUTPUT
174 | echo "EOF" >> $GITHUB_OUTPUT
175 |
176 | - name: Build binaries
177 | run: |
178 | chmod +x build.sh
179 | ./build.sh --release --version ${{ steps.get_version.outputs.version }}
180 |
181 | - name: Generate checksums
182 | run: |
183 | cd bin
184 | echo "### 🔒 SHA256 Checksums" > checksums.txt
185 | echo '```' >> checksums.txt
186 | sha256sum code-sandbox-mcp-* >> checksums.txt
187 | echo '```' >> checksums.txt
188 |
189 | - name: Create Release
190 | id: create_release
191 | uses: softprops/action-gh-release@v1
192 | with:
193 | tag_name: v${{ steps.get_version.outputs.version }}
194 | name: Release v${{ steps.get_version.outputs.version }}
195 | draft: false
196 | prerelease: ${{ github.event.inputs.prerelease == 'true' }}
197 | files: |
198 | bin/code-sandbox-mcp-linux-amd64
199 | bin/code-sandbox-mcp-linux-arm64
200 | bin/code-sandbox-mcp-darwin-amd64
201 | bin/code-sandbox-mcp-darwin-arm64
202 | bin/code-sandbox-mcp-windows-amd64.exe
203 | bin/code-sandbox-mcp-windows-arm64.exe
204 | body: |
205 | ## 🎉 Release v${{ steps.get_version.outputs.version }}
206 |
207 | ${{ steps.get_version.outputs.changes }}
208 |
209 | ### 📦 Included Binaries
210 | - 🐧 Linux (amd64, arm64)
211 | - 🍎 macOS (amd64, arm64)
212 | - 🪟 Windows (amd64, arm64)
213 |
214 | $(cat bin/checksums.txt)
```