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