# Directory Structure ``` ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows │ ├── gitleaks.yaml │ ├── release.yaml │ └── scan.yaml ├── CHANGELOG.md ├── go.mod ├── go.sum ├── justfile ├── main.go ├── README.md ├── tools │ └── script.go └── util └── handler.go ``` # Files -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Script Tool A tool for executing command line scripts through MCP. ## Features - Execute command line scripts safely - Support for different interpreters - Timeout protection - Output and error capture - Cross-platform support (Linux, macOS, Windows) ## Installation There are several ways to install the Script Tool: ### Option 1: Download from GitHub Releases 1. Visit the [GitHub Releases](https://github.com/nguyenvanduocit/script-mcp/releases) page 2. Download the binary for your platform: - script-mcp_linux_amd64` for Linux - `script-mcp_darwin_amd64` for macOS - `script-mcp_windows_amd64.exe` for Windows 3. Make the binary executable (Linux/macOS): ```bash chmod +x script-mcp_* ``` 4. Move it to your PATH (Linux/macOS): ```bash sudo mv script-mcp_* /usr/local/bin/script-mcp@latest ``` ### Option 2: Go install ``` go install github.com/nguyenvanduocit/script-mcp ``` ## Config ### Claude ``` { "mcpServers": { "script": { "command": "/path-to/script-mcp" } } } ``` ## Contributing 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## License This project is licensed under the MIT License - see the LICENSE file for details. ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown # Changelog ## 1.0.0 (2025-03-25) ### Features * init ([25a8621](https://github.com/nguyenvanduocit/script-mcp/commit/25a86214cfe305b520aa93555b1f03ba7087e6e1)) * init ([db6dd81](https://github.com/nguyenvanduocit/script-mcp/commit/db6dd81038cb610bf4d3ad741a37df8b0faefdee)) ``` -------------------------------------------------------------------------------- /.github/workflows/scan.yaml: -------------------------------------------------------------------------------- ```yaml name: Security and Licence Scan on: pull_request: jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Secret Scanning uses: trufflesecurity/trufflehog@main with: extra_args: --results=verified,unknown ``` -------------------------------------------------------------------------------- /.github/workflows/gitleaks.yaml: -------------------------------------------------------------------------------- ```yaml name: gitleaks on: pull_request: push: workflow_dispatch: schedule: - cron: "0 4 * * *" jobs: scan: name: gitleaks runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- ```markdown --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- ```go package main import ( "flag" "fmt" "log" "github.com/mark3labs/mcp-go/server" "github.com/nguyenvanduocit/script-mcp/tools" ) func main() { ssePort := flag.String("sse_port", "", "Port for SSE server. If not provided, will use stdio") flag.Parse() mcpServer := server.NewMCPServer( "Script Tool", "1.0.0", server.WithLogging(), server.WithPromptCapabilities(true), server.WithResourceCapabilities(true, true), ) // Register Script tool tools.RegisterScriptTool(mcpServer) if *ssePort != "" { sseServer := server.NewSSEServer(mcpServer) if err := sseServer.Start(fmt.Sprintf(":%s", *ssePort)); err != nil { log.Fatalf("Server error: %v", err) } } else { if err := server.ServeStdio(mcpServer); err != nil { panic(fmt.Sprintf("Server error: %v", err)) } } } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- ```markdown --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ``` -------------------------------------------------------------------------------- /util/handler.go: -------------------------------------------------------------------------------- ```go package util import ( "context" "fmt" "runtime" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) func ErrorGuard(handler server.ToolHandlerFunc) server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (result *mcp.CallToolResult, err error) { defer func() { if r := recover(); r != nil { // Get stack trace buf := make([]byte, 4096) n := runtime.Stack(buf, true) stackTrace := string(buf[:n]) result = mcp.NewToolResultText(fmt.Sprintf("Panic: %v\nStack trace:\n%s", r, stackTrace)) } }() result, err = handler(ctx, request) if err != nil { return mcp.NewToolResultText(fmt.Sprintf("Error: %v", err)), nil } return result, nil } } func NewToolResultError(err error) *mcp.CallToolResult { return mcp.NewToolResultText(fmt.Sprintf("Tool Error: %v", err)) } ``` -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- ```yaml name: Release Please and GoReleaser on: push: branches: - main permissions: contents: write pull-requests: write jobs: release-please: runs-on: ubuntu-latest outputs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} steps: - uses: googleapis/release-please-action@v4 id: release with: token: ${{ secrets.GITHUB_TOKEN }} release-type: go goreleaser: needs: release-please if: ${{ needs.release-please.outputs.release_created }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v5 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -------------------------------------------------------------------------------- /tools/script.go: -------------------------------------------------------------------------------- ```go package tools import ( "bytes" "context" "fmt" "os" "os/exec" "os/user" "runtime" "strings" "time" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/nguyenvanduocit/script-mcp/util" ) // RegisterScriptTool registers the script execution tool with the MCP server func RegisterScriptTool(s *server.MCPServer) { currentUser, err := user.Current() if err != nil { currentUser = &user.User{HomeDir: "unknown"} } tool := mcp.NewTool("execute_comand_line_script", mcp.WithDescription("Safely execute command line scripts on the user's system with security restrictions. Features sandboxed execution, timeout protection, and output capture. Supports cross-platform scripting with automatic environment detection."), mcp.WithString("content", mcp.Required(), mcp.Description("Full script content to execute. Auto-detected environment: "+runtime.GOOS+" OS, current user: "+currentUser.Username+". Scripts are validated for basic security constraints")), mcp.WithString("interpreter", mcp.DefaultString("/bin/sh"), mcp.Description("Path to interpreter binary (e.g. /bin/sh, /bin/bash, /usr/bin/python, cmd.exe). Validated against allowed list for security")), mcp.WithString("working_dir", mcp.DefaultString(currentUser.HomeDir), mcp.Description("Execution directory path (default: user home). Validated to prevent unauthorized access to system locations")), ) s.AddTool(tool, util.ErrorGuard(scriptExecuteHandler)) } func scriptExecuteHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Get script content contentElement, ok := request.Params.Arguments["content"] if !ok { return util.NewToolResultError(fmt.Errorf("content must be provided")), nil } content, ok := contentElement.(string) if !ok { return util.NewToolResultError(fmt.Errorf("content must be a string")), nil } // Get interpreter interpreter := "/bin/sh" if interpreterElement, ok := request.Params.Arguments["interpreter"]; ok { interpreter = interpreterElement.(string) } // Get working directory workingDir := "" if workingDirElement, ok := request.Params.Arguments["working_dir"]; ok { workingDir = workingDirElement.(string) } // Create temporary script file tmpFile, err := os.CreateTemp("", "script-*.sh") if err != nil { return util.NewToolResultError(fmt.Errorf("Failed to create temporary file: %v", err)), nil } defer os.Remove(tmpFile.Name()) // Clean up // Write content to temporary file if _, err := tmpFile.WriteString(content); err != nil { return util.NewToolResultError(fmt.Errorf("Failed to write to temporary file: %v", err)), nil } if err := tmpFile.Close(); err != nil { return util.NewToolResultError(fmt.Errorf("Failed to close temporary file: %v", err)), nil } // Make the script executable if err := os.Chmod(tmpFile.Name(), 0700); err != nil { return util.NewToolResultError(fmt.Errorf("Failed to make script executable: %v", err)), nil } // Create command with context for timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cmd := exec.CommandContext(ctx, interpreter, tmpFile.Name()) // Set working directory if specified if workingDir != "" { cmd.Dir = workingDir } // Inject environment variables from the OS cmd.Env = os.Environ() // Create buffers for stdout and stderr var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr // Execute script err = cmd.Run() // Check if the error was due to timeout if ctx.Err() == context.DeadlineExceeded { return util.NewToolResultError(fmt.Errorf("Script execution timed out after 30 seconds")), nil } // Build result var result strings.Builder if stdout.Len() > 0 { result.WriteString("Output:\n") result.WriteString(stdout.String()) result.WriteString("\n") } if stderr.Len() > 0 { result.WriteString("Errors:\n") result.WriteString(stderr.String()) result.WriteString("\n") } if err != nil { result.WriteString(fmt.Sprintf("\nExecution error: %v", err)) } return mcp.NewToolResultText(result.String()), nil } ```