Skip to content

Commit 63b23fb

Browse files
committed
Add shell MCP
Signed-off-by: Ettore Di Giacinto <[email protected]>
1 parent 3d8ec60 commit 63b23fb

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,69 @@ mcp:
259259
}
260260
```
261261

262+
### 🐚 Shell Server
263+
264+
A shell script execution server that allows AI models to execute shell scripts and commands.
265+
266+
**Features:**
267+
- Execute shell scripts with full shell capabilities
268+
- Configurable shell command (default: `sh -c`)
269+
- Separate stdout and stderr capture
270+
- Exit code reporting
271+
- Configurable timeout (default: 30 seconds)
272+
- JSON schema validation for inputs/outputs
273+
274+
**Tool:**
275+
- `execute_command` - Execute a shell script and return the output, exit code, and any errors
276+
277+
**Configuration:**
278+
- `SHELL_CMD` - Environment variable to set the shell command to use (default: `sh`). Can include arguments, e.g., `bash -x` or `zsh`
279+
280+
**Input Format:**
281+
```json
282+
{
283+
"script": "ls -la /tmp",
284+
"timeout": 30
285+
}
286+
```
287+
288+
**Output Format:**
289+
```json
290+
{
291+
"script": "ls -la /tmp",
292+
"stdout": "total 1234\ndrwxrwxrwt...",
293+
"stderr": "",
294+
"exit_code": 0,
295+
"success": true,
296+
"error": ""
297+
}
298+
```
299+
300+
**Docker Image:**
301+
```bash
302+
docker run -e SHELL_CMD=bash ghcr.io/mudler/mcps/shell:latest
303+
```
304+
305+
**LocalAI configuration ( to add to the model config):**
306+
```yaml
307+
mcp:
308+
stdio: |
309+
{
310+
"mcpServers": {
311+
"shell": {
312+
"command": "docker",
313+
"env": {
314+
"SHELL_CMD": "bash"
315+
},
316+
"args": [
317+
"run", "-i", "--rm",
318+
"ghcr.io/mudler/mcps/shell:master"
319+
]
320+
}
321+
}
322+
}
323+
```
324+
262325
### 🔧 Script Runner Server
263326

264327
A flexible script and program execution server that allows AI models to run pre-defined scripts and programs as tools. Scripts can be defined inline or via file paths, and programs can be executed directly.
@@ -376,6 +439,7 @@ make dev
376439
make MCP_SERVER=duckduckgo build
377440
make MCP_SERVER=weather build
378441
make MCP_SERVER=memory build
442+
make MCP_SERVER=shell build
379443
make MCP_SERVER=scripts build
380444
381445
# Run tests and checks
@@ -435,6 +499,9 @@ Docker images are automatically built and pushed to GitHub Container Registry:
435499
- `ghcr.io/mudler/mcps/memory:latest` - Latest Memory server
436500
- `ghcr.io/mudler/mcps/memory:v1.0.0` - Tagged versions
437501
- `ghcr.io/mudler/mcps/memory:master` - Development versions
502+
- `ghcr.io/mudler/mcps/shell:latest` - Latest Shell server
503+
- `ghcr.io/mudler/mcps/shell:v1.0.0` - Tagged versions
504+
- `ghcr.io/mudler/mcps/shell:master` - Development versions
438505
- `ghcr.io/mudler/mcps/homeassistant:latest` - Latest Home Assistant server
439506
- `ghcr.io/mudler/mcps/homeassistant:v1.0.0` - Tagged versions
440507
- `ghcr.io/mudler/mcps/homeassistant:master` - Development versions

shell/main.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"log"
7+
"os"
8+
"os/exec"
9+
"strings"
10+
"time"
11+
12+
"github.com/modelcontextprotocol/go-sdk/mcp"
13+
)
14+
15+
// Input type for executing shell scripts
16+
type ExecuteCommandInput struct {
17+
Script string `json:"script" jsonschema:"the shell script to execute"`
18+
Timeout int `json:"timeout,omitempty" jsonschema:"optional timeout in seconds (default: 30)"`
19+
}
20+
21+
// Output type for script execution results
22+
type ExecuteCommandOutput struct {
23+
Script string `json:"script" jsonschema:"the script that was executed"`
24+
Stdout string `json:"stdout" jsonschema:"standard output from the script"`
25+
Stderr string `json:"stderr" jsonschema:"standard error from the script"`
26+
ExitCode int `json:"exit_code" jsonschema:"exit code of the script (0 means success)"`
27+
Success bool `json:"success" jsonschema:"whether the script executed successfully"`
28+
Error string `json:"error,omitempty" jsonschema:"error message if execution failed"`
29+
}
30+
31+
// getShellCommand returns the shell command to use, defaulting to "sh" if not set
32+
func getShellCommand() string {
33+
shellCmd := os.Getenv("SHELL_CMD")
34+
if shellCmd == "" {
35+
shellCmd = "sh -c"
36+
}
37+
return shellCmd
38+
}
39+
40+
// ExecuteCommand executes a shell script and returns the output
41+
func ExecuteCommand(ctx context.Context, req *mcp.CallToolRequest, input ExecuteCommandInput) (
42+
*mcp.CallToolResult,
43+
ExecuteCommandOutput,
44+
error,
45+
) {
46+
// Set default timeout if not provided
47+
timeout := input.Timeout
48+
if timeout <= 0 {
49+
timeout = 30
50+
}
51+
52+
// Create a context with timeout
53+
cmdCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
54+
defer cancel()
55+
56+
// Get shell command from environment variable (default: "sh")
57+
shellCmd := getShellCommand()
58+
59+
// Parse shell command - support both single command and command with args
60+
shellParts := strings.Fields(shellCmd)
61+
shellExec := shellParts[0]
62+
shellArgs := append(shellParts[1:], input.Script)
63+
64+
// Execute script using the configured shell
65+
cmd := exec.CommandContext(cmdCtx, shellExec, shellArgs...)
66+
67+
// Create buffers to capture stdout and stderr separately
68+
var stdoutBuf, stderrBuf bytes.Buffer
69+
cmd.Stdout = &stdoutBuf
70+
cmd.Stderr = &stderrBuf
71+
72+
// Execute command
73+
err := cmd.Run()
74+
75+
exitCode := 0
76+
success := true
77+
errorMsg := ""
78+
79+
if err != nil {
80+
success = false
81+
errorMsg = err.Error()
82+
83+
// Try to get exit code if available
84+
if exitError, ok := err.(*exec.ExitError); ok {
85+
exitCode = exitError.ExitCode()
86+
} else {
87+
// Context timeout or other error
88+
if cmdCtx.Err() == context.DeadlineExceeded {
89+
errorMsg = "Command timed out"
90+
}
91+
exitCode = -1
92+
}
93+
}
94+
95+
output := ExecuteCommandOutput{
96+
Script: input.Script,
97+
Stdout: stdoutBuf.String(),
98+
Stderr: stderrBuf.String(),
99+
ExitCode: exitCode,
100+
Success: success,
101+
Error: errorMsg,
102+
}
103+
104+
return nil, output, nil
105+
}
106+
107+
func main() {
108+
// Create MCP server for shell command execution
109+
server := mcp.NewServer(&mcp.Implementation{
110+
Name: "shell",
111+
Version: "v1.0.0",
112+
}, nil)
113+
114+
// Add tool for executing shell scripts
115+
mcp.AddTool(server, &mcp.Tool{
116+
Name: "execute_command",
117+
Description: "Execute a shell script and return the output, exit code, and any errors. The shell command can be configured via SHELL_CMD environment variable (default: 'sh')",
118+
}, ExecuteCommand)
119+
120+
// Run the server
121+
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
122+
log.Fatal(err)
123+
}
124+
}

0 commit comments

Comments
 (0)