Skip to content

Commit ad0fdca

Browse files
committed
Add ssh MCP
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 63b23fb commit ad0fdca

5 files changed

Lines changed: 411 additions & 4 deletions

File tree

README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,101 @@ mcp:
322322
}
323323
```
324324

325+
### 🔐 SSH Server
326+
327+
An SSH server that allows AI models to connect to remote SSH hosts and execute shell scripts.
328+
329+
**Features:**
330+
- Connect to remote SSH hosts
331+
- Execute shell scripts on remote hosts
332+
- Support for password and key-based authentication
333+
- Configurable remote shell command (default: `sh -c`)
334+
- Separate stdout and stderr capture
335+
- Exit code reporting
336+
- Configurable timeout (default: 30 seconds)
337+
- JSON schema validation for inputs/outputs
338+
339+
**Tool:**
340+
- `execute_script` - Execute a shell script on a remote SSH host and return the output, exit code, and any errors
341+
342+
**Configuration:**
343+
- `SSH_HOST` - Default SSH host (can be overridden per request)
344+
- `SSH_PORT` - Default SSH port (default: 22)
345+
- `SSH_USER` - Default SSH username (can be overridden per request)
346+
- `SSH_PASSWORD` - Default SSH password (can be overridden per request, or use SSH_KEY_PATH)
347+
- `SSH_KEY_PATH` - Path to SSH private key file (alternative to password authentication)
348+
- `SSH_KEY_PASSPHRASE` - Passphrase for encrypted SSH private key (if needed)
349+
- `SSH_SHELL_CMD` - Remote shell command to use (default: `sh -c`)
350+
351+
**Input Format:**
352+
```json
353+
{
354+
"host": "example.com",
355+
"port": 22,
356+
"user": "username",
357+
"password": "password",
358+
"script": "ls -la /tmp",
359+
"timeout": 30
360+
}
361+
```
362+
363+
Or using key-based authentication:
364+
```json
365+
{
366+
"host": "example.com",
367+
"user": "username",
368+
"key_path": "/path/to/private/key",
369+
"script": "ls -la /tmp",
370+
"timeout": 30
371+
}
372+
```
373+
374+
**Output Format:**
375+
```json
376+
{
377+
"host": "example.com",
378+
"script": "ls -la /tmp",
379+
"stdout": "total 1234\ndrwxrwxrwt...",
380+
"stderr": "",
381+
"exit_code": 0,
382+
"success": true,
383+
"error": ""
384+
}
385+
```
386+
387+
**Docker Image:**
388+
```bash
389+
docker run -e SSH_HOST=example.com -e SSH_USER=user -e SSH_PASSWORD=pass ghcr.io/mudler/mcps/ssh:latest
390+
```
391+
392+
Or with key-based authentication:
393+
```bash
394+
docker run -e SSH_HOST=example.com -e SSH_USER=user -e SSH_KEY_PATH=/path/to/key -v /host/keys:/path/to/key ghcr.io/mudler/mcps/ssh:latest
395+
```
396+
397+
**LocalAI configuration ( to add to the model config):**
398+
```yaml
399+
mcp:
400+
stdio: |
401+
{
402+
"mcpServers": {
403+
"ssh": {
404+
"command": "docker",
405+
"env": {
406+
"SSH_HOST": "example.com",
407+
"SSH_USER": "username",
408+
"SSH_PASSWORD": "password",
409+
"SSH_SHELL_CMD": "bash -c"
410+
},
411+
"args": [
412+
"run", "-i", "--rm",
413+
"ghcr.io/mudler/mcps/ssh:master"
414+
]
415+
}
416+
}
417+
}
418+
```
419+
325420
### 🔧 Script Runner Server
326421

327422
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.
@@ -440,6 +535,7 @@ make MCP_SERVER=duckduckgo build
440535
make MCP_SERVER=weather build
441536
make MCP_SERVER=memory build
442537
make MCP_SERVER=shell build
538+
make MCP_SERVER=ssh build
443539
make MCP_SERVER=scripts build
444540
445541
# Run tests and checks
@@ -502,6 +598,9 @@ Docker images are automatically built and pushed to GitHub Container Registry:
502598
- `ghcr.io/mudler/mcps/shell:latest` - Latest Shell server
503599
- `ghcr.io/mudler/mcps/shell:v1.0.0` - Tagged versions
504600
- `ghcr.io/mudler/mcps/shell:master` - Development versions
601+
- `ghcr.io/mudler/mcps/ssh:latest` - Latest SSH server
602+
- `ghcr.io/mudler/mcps/ssh:v1.0.0` - Tagged versions
603+
- `ghcr.io/mudler/mcps/ssh:master` - Development versions
505604
- `ghcr.io/mudler/mcps/homeassistant:latest` - Latest Home Assistant server
506605
- `ghcr.io/mudler/mcps/homeassistant:v1.0.0` - Tagged versions
507606
- `ghcr.io/mudler/mcps/homeassistant:master` - Development versions

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
1818
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
1919
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
20-
golang.org/x/net v0.25.0 // indirect
21-
golang.org/x/sys v0.27.0 // indirect
20+
golang.org/x/crypto v0.46.0 // indirect
21+
golang.org/x/net v0.47.0 // indirect
22+
golang.org/x/sys v0.39.0 // indirect
2223
)

go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfG
5858
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
5959
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
6060
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
61+
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
62+
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
6163
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
6264
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
6365
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -77,6 +79,7 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
7779
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
7880
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
7981
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
82+
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
8083
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
8184
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
8285
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -95,6 +98,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
9598
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
9699
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
97100
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
101+
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
102+
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
98103
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
99104
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
100105
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

shell/main.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,15 @@ func ExecuteCommand(ctx context.Context, req *mcp.CallToolRequest, input Execute
5858

5959
// Parse shell command - support both single command and command with args
6060
shellParts := strings.Fields(shellCmd)
61+
6162
shellExec := shellParts[0]
62-
shellArgs := append(shellParts[1:], input.Script)
63+
shellArgs := []string{}
64+
65+
if len(shellParts) > 1 {
66+
shellArgs = append(shellParts[1:], input.Script)
67+
} else {
68+
shellArgs = []string{"-c", input.Script}
69+
}
6370

6471
// Execute script using the configured shell
6572
cmd := exec.CommandContext(cmdCtx, shellExec, shellArgs...)
@@ -111,9 +118,14 @@ func main() {
111118
Version: "v1.0.0",
112119
}, nil)
113120

121+
configurableName := os.GetEnv("TOOL_NAME")
122+
if configurableName == "" {
123+
configurableName = "execute_command"
124+
}
125+
114126
// Add tool for executing shell scripts
115127
mcp.AddTool(server, &mcp.Tool{
116-
Name: "execute_command",
128+
Name: configurableName,
117129
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')",
118130
}, ExecuteCommand)
119131

0 commit comments

Comments
 (0)