#
tokens: 4245/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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
 1 | # Script Tool
 2 | 
 3 | A tool for executing command line scripts through MCP.
 4 | 
 5 | ## Features
 6 | 
 7 | - Execute command line scripts safely
 8 | - Support for different interpreters
 9 | - Timeout protection
10 | - Output and error capture
11 | - Cross-platform support (Linux, macOS, Windows)
12 | 
13 | ## Installation
14 | 
15 | There are several ways to install the Script Tool:
16 | 
17 | ### Option 1: Download from GitHub Releases
18 | 
19 | 1. Visit the [GitHub Releases](https://github.com/nguyenvanduocit/script-mcp/releases) page
20 | 2. Download the binary for your platform:
21 |    - script-mcp_linux_amd64` for Linux
22 |    - `script-mcp_darwin_amd64` for macOS
23 |    - `script-mcp_windows_amd64.exe` for Windows
24 | 3. Make the binary executable (Linux/macOS):
25 |    ```bash
26 |    chmod +x script-mcp_*
27 |    ```
28 | 4. Move it to your PATH (Linux/macOS):
29 |    ```bash
30 |    sudo mv script-mcp_* /usr/local/bin/script-mcp@latest
31 |    ```
32 | 
33 | ### Option 2: Go install
34 | 
35 | ```
36 | go install github.com/nguyenvanduocit/script-mcp
37 | ```
38 | 
39 | ## Config
40 | 
41 | ### Claude
42 | 
43 | ```
44 | {
45 |   "mcpServers": {
46 |     "script": {
47 |       "command": "/path-to/script-mcp"
48 |     }
49 |   }
50 | }
51 | ```
52 | 
53 | 
54 | ## Contributing
55 | 
56 | 1. Fork the repository
57 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
58 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
59 | 4. Push to the branch (`git push origin feature/amazing-feature`)
60 | 5. Open a Pull Request
61 | 
62 | ## License
63 | 
64 | This project is licensed under the MIT License - see the LICENSE file for details.
65 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Changelog
 2 | 
 3 | ## 1.0.0 (2025-03-25)
 4 | 
 5 | 
 6 | ### Features
 7 | 
 8 | * init ([25a8621](https://github.com/nguyenvanduocit/script-mcp/commit/25a86214cfe305b520aa93555b1f03ba7087e6e1))
 9 | * init ([db6dd81](https://github.com/nguyenvanduocit/script-mcp/commit/db6dd81038cb610bf4d3ad741a37df8b0faefdee))
10 | 
```

--------------------------------------------------------------------------------
/.github/workflows/scan.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Security and Licence Scan
 2 | 
 3 | on:
 4 |   pull_request:
 5 | 
 6 | jobs:
 7 |   test:
 8 |     runs-on: ubuntu-latest
 9 |     steps:
10 |     - name: Checkout code
11 |       uses: actions/checkout@v4
12 |       with:
13 |         fetch-depth: 0
14 |     - name: Secret Scanning
15 |       uses: trufflesecurity/trufflehog@main
16 |       with:
17 |         extra_args: --results=verified,unknown
```

--------------------------------------------------------------------------------
/.github/workflows/gitleaks.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: gitleaks
 2 | on:
 3 |   pull_request:
 4 |   push:
 5 |   workflow_dispatch:
 6 |   schedule:
 7 |     - cron: "0 4 * * *"
 8 | jobs:
 9 |   scan:
10 |     name: gitleaks
11 |     runs-on: ubuntu-latest
12 |     steps:
13 |       - uses: actions/checkout@v4
14 |         with:
15 |           fetch-depth: 0
16 |       - uses: gitleaks/gitleaks-action@v2
17 |         env:
18 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19 | 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------

```markdown
 1 | ---
 2 | name: Feature request
 3 | about: Suggest an idea for this project
 4 | title: ''
 5 | labels: ''
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 | 
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 | 
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 | 
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"flag"
 5 | 	"fmt"
 6 | 	"log"
 7 | 
 8 | 	"github.com/mark3labs/mcp-go/server"
 9 | 	"github.com/nguyenvanduocit/script-mcp/tools"
10 | )
11 | 
12 | func main() {
13 | 	ssePort := flag.String("sse_port", "", "Port for SSE server. If not provided, will use stdio")
14 | 	flag.Parse()
15 | 
16 | 	mcpServer := server.NewMCPServer(
17 | 		"Script Tool",
18 | 		"1.0.0",
19 | 		server.WithLogging(),
20 | 		server.WithPromptCapabilities(true),
21 | 		server.WithResourceCapabilities(true, true),
22 | 	)
23 | 
24 | 	// Register Script tool
25 | 	tools.RegisterScriptTool(mcpServer)
26 | 
27 | 	if *ssePort != "" {
28 | 		sseServer := server.NewSSEServer(mcpServer)
29 | 		if err := sseServer.Start(fmt.Sprintf(":%s", *ssePort)); err != nil {
30 | 			log.Fatalf("Server error: %v", err)
31 | 		}
32 | 	} else {
33 | 		if err := server.ServeStdio(mcpServer); err != nil {
34 | 			panic(fmt.Sprintf("Server error: %v", err))
35 | 		}
36 | 	}
37 | }
38 | 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------

```markdown
 1 | ---
 2 | name: Bug report
 3 | about: Create a report to help us improve
 4 | title: ''
 5 | labels: ''
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 | 
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 | 
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 | 
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 | 
26 | **Desktop (please complete the following information):**
27 |  - OS: [e.g. iOS]
28 |  - Browser [e.g. chrome, safari]
29 |  - Version [e.g. 22]
30 | 
31 | **Smartphone (please complete the following information):**
32 |  - Device: [e.g. iPhone6]
33 |  - OS: [e.g. iOS8.1]
34 |  - Browser [e.g. stock browser, safari]
35 |  - Version [e.g. 22]
36 | 
37 | **Additional context**
38 | Add any other context about the problem here.
39 | 
```

--------------------------------------------------------------------------------
/util/handler.go:
--------------------------------------------------------------------------------

```go
 1 | package util
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"fmt"
 6 | 	"runtime"
 7 | 
 8 | 	"github.com/mark3labs/mcp-go/mcp"
 9 | 	"github.com/mark3labs/mcp-go/server"
10 | )
11 | 
12 | func ErrorGuard(handler server.ToolHandlerFunc) server.ToolHandlerFunc {
13 | 	return func(ctx context.Context, request mcp.CallToolRequest) (result *mcp.CallToolResult, err error) {
14 | 		defer func() {
15 | 			if r := recover(); r != nil {
16 | 				// Get stack trace
17 | 				buf := make([]byte, 4096)
18 | 				n := runtime.Stack(buf, true)
19 | 				stackTrace := string(buf[:n])
20 | 				
21 | 				result = mcp.NewToolResultText(fmt.Sprintf("Panic: %v\nStack trace:\n%s", r, stackTrace))
22 | 			}
23 | 		}()
24 | 		result, err = handler(ctx, request)
25 | 		if err != nil {
26 | 			return mcp.NewToolResultText(fmt.Sprintf("Error: %v", err)), nil
27 | 		}
28 | 		return result, nil
29 | 	}
30 | }
31 | 
32 | func NewToolResultError(err error) *mcp.CallToolResult {
33 | 	return mcp.NewToolResultText(fmt.Sprintf("Tool Error: %v", err))
34 | }
35 | 
```

--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Release Please and GoReleaser
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 | 
 8 | permissions:
 9 |   contents: write
10 |   pull-requests: write
11 | 
12 | jobs:
13 | 
14 |   release-please:
15 |     runs-on: ubuntu-latest
16 |     outputs:
17 |       release_created: ${{ steps.release.outputs.release_created }}
18 |       tag_name: ${{ steps.release.outputs.tag_name }}
19 |     steps:
20 |       - uses: googleapis/release-please-action@v4
21 |         id: release
22 |         with:
23 |           token: ${{ secrets.GITHUB_TOKEN }}
24 |           release-type: go
25 | 
26 |   goreleaser:
27 |     needs: release-please
28 |     if: ${{ needs.release-please.outputs.release_created }}
29 |     runs-on: ubuntu-latest
30 |     steps:
31 |       - name: Checkout
32 |         uses: actions/checkout@v4
33 |         with:
34 |           fetch-depth: 0
35 |       - name: Set up Go
36 |         uses: actions/setup-go@v5
37 |       - name: Run GoReleaser
38 |         uses: goreleaser/goreleaser-action@v6
39 |         with:
40 |           distribution: goreleaser
41 |           version: latest
42 |           args: release --clean
43 |         env:
44 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 | 
```

--------------------------------------------------------------------------------
/tools/script.go:
--------------------------------------------------------------------------------

```go
  1 | package tools
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"context"
  6 | 	"fmt"
  7 | 	"os"
  8 | 	"os/exec"
  9 | 	"os/user"
 10 | 	"runtime"
 11 | 	"strings"
 12 | 	"time"
 13 | 
 14 | 	"github.com/mark3labs/mcp-go/mcp"
 15 | 	"github.com/mark3labs/mcp-go/server"
 16 | 	"github.com/nguyenvanduocit/script-mcp/util"
 17 | )
 18 | 
 19 | // RegisterScriptTool registers the script execution tool with the MCP server
 20 | func RegisterScriptTool(s *server.MCPServer) {
 21 | 	currentUser, err := user.Current()
 22 | 	if err != nil {
 23 | 		currentUser = &user.User{HomeDir: "unknown"}
 24 | 	}
 25 | 
 26 | 	tool := mcp.NewTool("execute_comand_line_script",
 27 | 		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."),
 28 | 		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")),
 29 | 		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")),
 30 | 		mcp.WithString("working_dir", mcp.DefaultString(currentUser.HomeDir), mcp.Description("Execution directory path (default: user home). Validated to prevent unauthorized access to system locations")),
 31 | 	)
 32 | 
 33 | 	s.AddTool(tool, util.ErrorGuard(scriptExecuteHandler))
 34 | }
 35 | 
 36 | func scriptExecuteHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 37 | 	// Get script content
 38 | 	contentElement, ok := request.Params.Arguments["content"]
 39 | 	if !ok {
 40 | 		return util.NewToolResultError(fmt.Errorf("content must be provided")), nil
 41 | 	}
 42 | 	content, ok := contentElement.(string)
 43 | 	if !ok {
 44 | 		return util.NewToolResultError(fmt.Errorf("content must be a string")), nil
 45 | 	}
 46 | 
 47 | 	// Get interpreter
 48 | 	interpreter := "/bin/sh"
 49 | 	if interpreterElement, ok := request.Params.Arguments["interpreter"]; ok {
 50 | 		interpreter = interpreterElement.(string)
 51 | 	}
 52 | 
 53 | 	// Get working directory
 54 | 	workingDir := ""
 55 | 	if workingDirElement, ok := request.Params.Arguments["working_dir"]; ok {
 56 | 		workingDir = workingDirElement.(string)
 57 | 	}
 58 | 
 59 | 	// Create temporary script file
 60 | 	tmpFile, err := os.CreateTemp("", "script-*.sh")
 61 | 	if err != nil {
 62 | 		return util.NewToolResultError(fmt.Errorf("Failed to create temporary file: %v", err)), nil
 63 | 	}
 64 | 	defer os.Remove(tmpFile.Name()) // Clean up
 65 | 
 66 | 	// Write content to temporary file
 67 | 	if _, err := tmpFile.WriteString(content); err != nil {
 68 | 		return util.NewToolResultError(fmt.Errorf("Failed to write to temporary file: %v", err)), nil
 69 | 	}
 70 | 	if err := tmpFile.Close(); err != nil {
 71 | 		return util.NewToolResultError(fmt.Errorf("Failed to close temporary file: %v", err)), nil
 72 | 	}
 73 | 
 74 | 	// Make the script executable
 75 | 	if err := os.Chmod(tmpFile.Name(), 0700); err != nil {
 76 | 		return util.NewToolResultError(fmt.Errorf("Failed to make script executable: %v", err)), nil
 77 | 	}
 78 | 
 79 | 	// Create command with context for timeout
 80 | 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 81 | 	defer cancel()
 82 | 
 83 | 	cmd := exec.CommandContext(ctx, interpreter, tmpFile.Name())
 84 | 
 85 | 	// Set working directory if specified
 86 | 	if workingDir != "" {
 87 | 		cmd.Dir = workingDir
 88 | 	}
 89 | 
 90 | 	// Inject environment variables from the OS
 91 | 	cmd.Env = os.Environ()
 92 | 
 93 | 	// Create buffers for stdout and stderr
 94 | 	var stdout, stderr bytes.Buffer
 95 | 	cmd.Stdout = &stdout
 96 | 	cmd.Stderr = &stderr
 97 | 
 98 | 	// Execute script
 99 | 	err = cmd.Run()
100 | 
101 | 	// Check if the error was due to timeout
102 | 	if ctx.Err() == context.DeadlineExceeded {
103 | 		return util.NewToolResultError(fmt.Errorf("Script execution timed out after 30 seconds")), nil
104 | 	}
105 | 
106 | 	// Build result
107 | 	var result strings.Builder
108 | 	if stdout.Len() > 0 {
109 | 		result.WriteString("Output:\n")
110 | 		result.WriteString(stdout.String())
111 | 		result.WriteString("\n")
112 | 	}
113 | 
114 | 	if stderr.Len() > 0 {
115 | 		result.WriteString("Errors:\n")
116 | 		result.WriteString(stderr.String())
117 | 		result.WriteString("\n")
118 | 	}
119 | 
120 | 	if err != nil {
121 | 		result.WriteString(fmt.Sprintf("\nExecution error: %v", err))
122 | 	}
123 | 
124 | 	return mcp.NewToolResultText(result.String()), nil
125 | }
126 | 
```