slepp-ssh-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slepp_ssh_mcp-0.1.0/LICENSE +21 -0
- slepp_ssh_mcp-0.1.0/PKG-INFO +325 -0
- slepp_ssh_mcp-0.1.0/README.md +302 -0
- slepp_ssh_mcp-0.1.0/pyproject.toml +38 -0
- slepp_ssh_mcp-0.1.0/setup.cfg +4 -0
- slepp_ssh_mcp-0.1.0/src/slepp_ssh_mcp.egg-info/PKG-INFO +325 -0
- slepp_ssh_mcp-0.1.0/src/slepp_ssh_mcp.egg-info/SOURCES.txt +15 -0
- slepp_ssh_mcp-0.1.0/src/slepp_ssh_mcp.egg-info/dependency_links.txt +1 -0
- slepp_ssh_mcp-0.1.0/src/slepp_ssh_mcp.egg-info/entry_points.txt +2 -0
- slepp_ssh_mcp-0.1.0/src/slepp_ssh_mcp.egg-info/top_level.txt +1 -0
- slepp_ssh_mcp-0.1.0/src/ssh_mcp/__init__.py +5 -0
- slepp_ssh_mcp-0.1.0/src/ssh_mcp/__main__.py +13 -0
- slepp_ssh_mcp-0.1.0/src/ssh_mcp/observe.py +35 -0
- slepp_ssh_mcp-0.1.0/src/ssh_mcp/server.py +649 -0
- slepp_ssh_mcp-0.1.0/src/ssh_mcp/ssh.py +1938 -0
- slepp_ssh_mcp-0.1.0/tests/test_ssh_tools.py +833 -0
- slepp_ssh_mcp-0.1.0/tests/test_stdio_server.py +351 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slepp-ssh-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A stdio MCP server that exposes Bash-like SSH execution and sessions.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/slepp/ssh-mcp
|
|
7
|
+
Project-URL: Repository, https://github.com/slepp/ssh-mcp
|
|
8
|
+
Project-URL: Issues, https://github.com/slepp/ssh-mcp/issues
|
|
9
|
+
Keywords: mcp,ssh,scp,rsync,tmux
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Operating System :: POSIX
|
|
16
|
+
Classifier: Topic :: System :: Networking
|
|
17
|
+
Classifier: Topic :: System :: Shells
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# ssh-mcp
|
|
25
|
+
|
|
26
|
+
An MCP server that gives AI agents SSH access to remote machines through your local OpenSSH client. It wraps `ssh`, `scp`, and `rsync` so agents can run remote commands, transfer files, maintain persistent shell sessions, and set up port forwards — all using your existing SSH config, keys, and credentials.
|
|
27
|
+
|
|
28
|
+
## Why ssh-mcp?
|
|
29
|
+
|
|
30
|
+
- **Uses your local SSH** — host aliases, `~/.ssh/config`, `ProxyJump`, agent forwarding, and existing credentials all work naturally. No SSH libraries or key management.
|
|
31
|
+
- **Persistent sessions** — agents can keep a shell open across multiple tool calls, just like a human would. Sessions survive context window resets when you give them a `session_name`.
|
|
32
|
+
- **Observable** — every session records a transcript and optionally launches a detached tmux viewer so you can watch what the agent is doing in real time.
|
|
33
|
+
- **Permission-gatable** — port forwarding is a separate tool from command execution, so MCP clients can allow SSH access without allowing port forwards.
|
|
34
|
+
- **Pure Python** — no third-party runtime dependencies. Runs anywhere Python 3.10+ and OpenSSH are available.
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Python 3.10+
|
|
39
|
+
- `ssh` and `scp` on PATH (or set `SSH_MCP_SSH_BIN` / `SSH_MCP_SCP_BIN`)
|
|
40
|
+
- `rsync` on PATH (or set `SSH_MCP_RSYNC_BIN`) — only needed for `ssh_sync`
|
|
41
|
+
- `tmux` — optional, for live session observation
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
### With uv (recommended)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uvx --from git+https://github.com/slepp/ssh-mcp ssh-mcp
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or install persistently:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv tool install git+https://github.com/slepp/ssh-mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### With pip
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install git+https://github.com/slepp/ssh-mcp
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Setup
|
|
64
|
+
|
|
65
|
+
### Claude Code
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
claude mcp add --transport stdio --scope user ssh-mcp -- uvx --from git+https://github.com/slepp/ssh-mcp ssh-mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or commit a `.mcp.json` to share with your team:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"ssh-mcp": {
|
|
77
|
+
"type": "stdio",
|
|
78
|
+
"command": "uvx",
|
|
79
|
+
"args": ["--from", "git+https://github.com/slepp/ssh-mcp", "ssh-mcp"]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Codex CLI
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
codex mcp add ssh-mcp -- uvx --from git+https://github.com/slepp/ssh-mcp ssh-mcp
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### GitHub Copilot
|
|
92
|
+
|
|
93
|
+
Add to `~/.copilot/mcp-config.json` (or `.vscode/mcp.json` per-project):
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"ssh-mcp": {
|
|
99
|
+
"type": "stdio",
|
|
100
|
+
"command": "uvx",
|
|
101
|
+
"args": ["--from", "git+https://github.com/slepp/ssh-mcp", "ssh-mcp"]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Generic MCP client
|
|
108
|
+
|
|
109
|
+
Any stdio MCP client works. Point it at `uvx --from slepp-ssh-mcp ssh-mcp` or at a virtualenv's `ssh-mcp` entrypoint.
|
|
110
|
+
|
|
111
|
+
## How it works
|
|
112
|
+
|
|
113
|
+
ssh-mcp runs as a stdio process that your MCP client spawns. It receives JSON-RPC tool calls and translates them into local `ssh`/`scp`/`rsync` commands. Because it uses your local SSH binary, everything in your `~/.ssh/config` works — jump hosts, custom ports, key selection, `SSH_AUTH_SOCK`, proxy commands.
|
|
114
|
+
|
|
115
|
+
There are three modes of operation:
|
|
116
|
+
|
|
117
|
+
### One-off commands (`ssh_exec`)
|
|
118
|
+
|
|
119
|
+
Run a command, get stdout/stderr/exit code back. Works like `ssh host 'command'`.
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"target": "prod-web01",
|
|
124
|
+
"command": "systemctl status nginx",
|
|
125
|
+
"timeout": 10
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Use `cwd` to set the working directory, `env` to export variables, and `tty: true` for commands that need a terminal (like `sudo` with a password prompt). Note that `tty` merges stdout and stderr.
|
|
130
|
+
|
|
131
|
+
### Interactive sessions (`ssh_ensure_session` + `ssh_write_session` + `ssh_read_session`)
|
|
132
|
+
|
|
133
|
+
For multi-step work, open a persistent shell. The agent writes commands and reads output just like typing in a terminal.
|
|
134
|
+
|
|
135
|
+
**Start or reuse a session:**
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"target": "prod-web01",
|
|
140
|
+
"session_name": "deploy-api"
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Always use a descriptive `session_name`. It serves three purposes:
|
|
145
|
+
1. The agent can find the same session across multiple tool calls
|
|
146
|
+
2. The tmux observer window gets a human-readable name (e.g., `ssh-mcp-prod-web01-deploy-api`)
|
|
147
|
+
3. A different agent or conversation can recover the session by name
|
|
148
|
+
|
|
149
|
+
**Write a command:**
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"session_id": "a1b2c3d4e5f6",
|
|
154
|
+
"input": "cd /app && git pull\n",
|
|
155
|
+
"wait_seconds": 5
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Always include `\n` to press Enter. Use `\u0003` for Ctrl-C, `\u0004` for Ctrl-D. Set `wait_seconds` high enough for the command to produce output (default: 1 second).
|
|
160
|
+
|
|
161
|
+
**Read more output:**
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"session_id": "a1b2c3d4e5f6",
|
|
166
|
+
"wait_seconds": 10
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Check `pending_output_chars` in the response — if non-zero, call again to drain the buffer.
|
|
171
|
+
|
|
172
|
+
**Session lifecycle:**
|
|
173
|
+
- `ssh_ensure_session` is idempotent — call it at the start of each step
|
|
174
|
+
- Sessions auto-detect dead connections via SSH keepalive (~90 seconds)
|
|
175
|
+
- Response includes `uptime_seconds`, `idle_seconds`, and `exit_reason` for health monitoring
|
|
176
|
+
- Use `auto_close: true` for one-shot commands that should clean up when done
|
|
177
|
+
- Exited sessions are pruned from memory after 1 hour (5 minutes for `auto_close`)
|
|
178
|
+
- `cwd`, `env`, and `shell` only apply when creating a new session — they are ignored when reusing an existing one
|
|
179
|
+
|
|
180
|
+
### File transfer (`ssh_scp`, `ssh_sync`)
|
|
181
|
+
|
|
182
|
+
Copy files between local and remote machines.
|
|
183
|
+
|
|
184
|
+
**scp** — simple file/directory copy:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"target": "prod-web01",
|
|
189
|
+
"direction": "upload",
|
|
190
|
+
"sources": ["/local/path/app.tar.gz"],
|
|
191
|
+
"destination": "/tmp/"
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
For `upload`, `sources` are local paths and `destination` is remote. For `download`, it's reversed. The `target` parameter specifies the host — don't include the host in `sources` or `destination`.
|
|
196
|
+
|
|
197
|
+
**rsync** — incremental sync with `--delete` and `--exclude`:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"target": "prod-web01",
|
|
202
|
+
"direction": "upload",
|
|
203
|
+
"source": "./dist/",
|
|
204
|
+
"destination": "/var/www/app/",
|
|
205
|
+
"delete": true,
|
|
206
|
+
"exclude": ["*.log", ".git"]
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Port forwarding (`ssh_forward`)
|
|
211
|
+
|
|
212
|
+
Create local or remote port forwards. This is a separate tool from `ssh_exec` so MCP clients can grant SSH access without granting port forwarding.
|
|
213
|
+
|
|
214
|
+
**Local forward** — make a remote service reachable locally:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"target": "prod-web01",
|
|
219
|
+
"direction": "local",
|
|
220
|
+
"local_port": 15432,
|
|
221
|
+
"remote_host": "prod-db.internal",
|
|
222
|
+
"remote_port": 5432
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
This binds `localhost:15432` and tunnels it to `prod-db.internal:5432` through `prod-web01`.
|
|
227
|
+
|
|
228
|
+
**Remote forward** — expose a local service on the remote host:
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"target": "prod-web01",
|
|
233
|
+
"direction": "remote",
|
|
234
|
+
"local_port": 3000,
|
|
235
|
+
"remote_host": "localhost",
|
|
236
|
+
"remote_port": 8080
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Forwards bind to `127.0.0.1` by default. Set `bind_address: "0.0.0.0"` to expose on all interfaces (use with caution).
|
|
241
|
+
|
|
242
|
+
Use `ssh_list_forwards` to see active forwards and `ssh_stop_forward` to tear them down.
|
|
243
|
+
|
|
244
|
+
## Watching sessions
|
|
245
|
+
|
|
246
|
+
Every interactive session records a transcript to `~/.local/state/ssh-mcp/<session_id>/transcript.log`.
|
|
247
|
+
|
|
248
|
+
By default, sessions also launch a detached tmux window so you can watch in real time. The tmux session name includes the target and session name for easy identification:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# List ssh-mcp tmux sessions
|
|
252
|
+
tmux ls | grep ssh-mcp
|
|
253
|
+
|
|
254
|
+
# Attach to watch
|
|
255
|
+
tmux attach -t ssh-mcp-prod-web01-deploy-api
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
If you prefer not to use tmux, set `observer_mode: "transcript"` and tail the transcript file directly — the response includes an `observer_command` you can copy-paste.
|
|
259
|
+
|
|
260
|
+
The tmux observer is tied to the session lifecycle: stopping a session closes its tmux window. The MCP server also cleans up tmux on shutdown.
|
|
261
|
+
|
|
262
|
+
## Environment variables
|
|
263
|
+
|
|
264
|
+
| Variable | Default | Description |
|
|
265
|
+
|----------|---------|-------------|
|
|
266
|
+
| `SSH_MCP_SSH_BIN` | `ssh` | Path to the SSH client |
|
|
267
|
+
| `SSH_MCP_SCP_BIN` | `scp` | Path to the SCP client |
|
|
268
|
+
| `SSH_MCP_RSYNC_BIN` | `rsync` | Path to rsync |
|
|
269
|
+
| `SSH_MCP_TMUX_BIN` | `tmux` | Path to tmux |
|
|
270
|
+
| `SSH_MCP_STATE_DIR` | `~/.local/state/ssh-mcp` | Where transcripts are stored |
|
|
271
|
+
|
|
272
|
+
## Security
|
|
273
|
+
|
|
274
|
+
ssh-mcp is designed for single-developer use on your own machine. It runs SSH commands as your user with your credentials.
|
|
275
|
+
|
|
276
|
+
**What's protected:**
|
|
277
|
+
- Port forwarding flags (`-L`, `-R`, `-D`, `-W`) and dangerous SSH options (`ProxyCommand`, `LocalCommand`, `LocalForward`, `RemoteForward`, `DynamicForward`) are blocked in `extra_ssh_args`. The only way to create forwards is through the explicit `ssh_forward` tool, which MCP clients can permission-gate.
|
|
278
|
+
- Transcript files are created with mode `0600` and the state directory with `0700`.
|
|
279
|
+
- All command arguments use `shlex.quote()` to prevent shell injection. Subprocess calls use list arguments, never `shell=True`.
|
|
280
|
+
- Environment variable names are validated against `^[A-Za-z_][A-Za-z0-9_]*$`.
|
|
281
|
+
|
|
282
|
+
**What's not protected:**
|
|
283
|
+
- An agent with `ssh_exec` access can run arbitrary commands on any host your SSH config can reach. The security boundary is SSH itself (keys, known_hosts).
|
|
284
|
+
- Transcript files persist on disk after sessions end and may contain secrets (passwords typed at sudo prompts, API keys in output). Clean up `SSH_MCP_STATE_DIR` when you no longer need them.
|
|
285
|
+
- The `shell` parameter lets agents choose any remote executable. This is by design — the tool is for remote execution.
|
|
286
|
+
|
|
287
|
+
## Known limitations
|
|
288
|
+
|
|
289
|
+
- **POSIX-only remotes** — `cwd`, `env`, and `shell` wrapping assumes a POSIX shell on the remote side. Windows SSH targets need commands written for their shell.
|
|
290
|
+
- **PTY output** — interactive sessions use a PTY, so output includes terminal formatting (ANSI escape codes, command echo, line wrapping). This is intentional — it matches what a human would see.
|
|
291
|
+
- **No multiplexing** — each `ssh_exec` call opens a new SSH connection. If your agent runs many rapid commands to the same host, consider using a session instead, or configure `ControlMaster` in your `~/.ssh/config`.
|
|
292
|
+
- **Transcript growth** — transcripts grow without bound for long-running sessions. The response includes `transcript_size_bytes` so you can monitor this. Restart the session if it gets too large.
|
|
293
|
+
- **Forward connections are standalone** — each `ssh_forward` opens its own SSH connection. Forwards are not tied to sessions.
|
|
294
|
+
|
|
295
|
+
## Tools reference
|
|
296
|
+
|
|
297
|
+
| Tool | Description |
|
|
298
|
+
|------|-------------|
|
|
299
|
+
| `ssh_exec` | Run a one-off remote command |
|
|
300
|
+
| `ssh_scp` | Copy files via scp |
|
|
301
|
+
| `ssh_sync` | Incremental sync via rsync |
|
|
302
|
+
| `ssh_start_session` | Start a new interactive session |
|
|
303
|
+
| `ssh_ensure_session` | Reuse or start an interactive session (recommended) |
|
|
304
|
+
| `ssh_read_session` | Read output from a session |
|
|
305
|
+
| `ssh_write_session` | Write input to a session |
|
|
306
|
+
| `ssh_stop_session` | Stop a session |
|
|
307
|
+
| `ssh_list_sessions` | List tracked sessions |
|
|
308
|
+
| `ssh_forward` | Start a port forward |
|
|
309
|
+
| `ssh_list_forwards` | List tracked forwards |
|
|
310
|
+
| `ssh_stop_forward` | Stop a port forward |
|
|
311
|
+
|
|
312
|
+
All session and forward tools accept standard SSH connection parameters: `port`, `identity_file`, `known_hosts_file`, `strict_host_key_checking`, and `extra_ssh_args`.
|
|
313
|
+
|
|
314
|
+
## Development
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
python3 -m pip install build
|
|
318
|
+
python3 -m unittest discover -s tests -v
|
|
319
|
+
python3 -m compileall src
|
|
320
|
+
python3 -m build
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## License
|
|
324
|
+
|
|
325
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# ssh-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server that gives AI agents SSH access to remote machines through your local OpenSSH client. It wraps `ssh`, `scp`, and `rsync` so agents can run remote commands, transfer files, maintain persistent shell sessions, and set up port forwards — all using your existing SSH config, keys, and credentials.
|
|
4
|
+
|
|
5
|
+
## Why ssh-mcp?
|
|
6
|
+
|
|
7
|
+
- **Uses your local SSH** — host aliases, `~/.ssh/config`, `ProxyJump`, agent forwarding, and existing credentials all work naturally. No SSH libraries or key management.
|
|
8
|
+
- **Persistent sessions** — agents can keep a shell open across multiple tool calls, just like a human would. Sessions survive context window resets when you give them a `session_name`.
|
|
9
|
+
- **Observable** — every session records a transcript and optionally launches a detached tmux viewer so you can watch what the agent is doing in real time.
|
|
10
|
+
- **Permission-gatable** — port forwarding is a separate tool from command execution, so MCP clients can allow SSH access without allowing port forwards.
|
|
11
|
+
- **Pure Python** — no third-party runtime dependencies. Runs anywhere Python 3.10+ and OpenSSH are available.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Python 3.10+
|
|
16
|
+
- `ssh` and `scp` on PATH (or set `SSH_MCP_SSH_BIN` / `SSH_MCP_SCP_BIN`)
|
|
17
|
+
- `rsync` on PATH (or set `SSH_MCP_RSYNC_BIN`) — only needed for `ssh_sync`
|
|
18
|
+
- `tmux` — optional, for live session observation
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
### With uv (recommended)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uvx --from git+https://github.com/slepp/ssh-mcp ssh-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install persistently:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv tool install git+https://github.com/slepp/ssh-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### With pip
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install git+https://github.com/slepp/ssh-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
### Claude Code
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
claude mcp add --transport stdio --scope user ssh-mcp -- uvx --from git+https://github.com/slepp/ssh-mcp ssh-mcp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or commit a `.mcp.json` to share with your team:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"ssh-mcp": {
|
|
54
|
+
"type": "stdio",
|
|
55
|
+
"command": "uvx",
|
|
56
|
+
"args": ["--from", "git+https://github.com/slepp/ssh-mcp", "ssh-mcp"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Codex CLI
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
codex mcp add ssh-mcp -- uvx --from git+https://github.com/slepp/ssh-mcp ssh-mcp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### GitHub Copilot
|
|
69
|
+
|
|
70
|
+
Add to `~/.copilot/mcp-config.json` (or `.vscode/mcp.json` per-project):
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"ssh-mcp": {
|
|
76
|
+
"type": "stdio",
|
|
77
|
+
"command": "uvx",
|
|
78
|
+
"args": ["--from", "git+https://github.com/slepp/ssh-mcp", "ssh-mcp"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Generic MCP client
|
|
85
|
+
|
|
86
|
+
Any stdio MCP client works. Point it at `uvx --from slepp-ssh-mcp ssh-mcp` or at a virtualenv's `ssh-mcp` entrypoint.
|
|
87
|
+
|
|
88
|
+
## How it works
|
|
89
|
+
|
|
90
|
+
ssh-mcp runs as a stdio process that your MCP client spawns. It receives JSON-RPC tool calls and translates them into local `ssh`/`scp`/`rsync` commands. Because it uses your local SSH binary, everything in your `~/.ssh/config` works — jump hosts, custom ports, key selection, `SSH_AUTH_SOCK`, proxy commands.
|
|
91
|
+
|
|
92
|
+
There are three modes of operation:
|
|
93
|
+
|
|
94
|
+
### One-off commands (`ssh_exec`)
|
|
95
|
+
|
|
96
|
+
Run a command, get stdout/stderr/exit code back. Works like `ssh host 'command'`.
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"target": "prod-web01",
|
|
101
|
+
"command": "systemctl status nginx",
|
|
102
|
+
"timeout": 10
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use `cwd` to set the working directory, `env` to export variables, and `tty: true` for commands that need a terminal (like `sudo` with a password prompt). Note that `tty` merges stdout and stderr.
|
|
107
|
+
|
|
108
|
+
### Interactive sessions (`ssh_ensure_session` + `ssh_write_session` + `ssh_read_session`)
|
|
109
|
+
|
|
110
|
+
For multi-step work, open a persistent shell. The agent writes commands and reads output just like typing in a terminal.
|
|
111
|
+
|
|
112
|
+
**Start or reuse a session:**
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"target": "prod-web01",
|
|
117
|
+
"session_name": "deploy-api"
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Always use a descriptive `session_name`. It serves three purposes:
|
|
122
|
+
1. The agent can find the same session across multiple tool calls
|
|
123
|
+
2. The tmux observer window gets a human-readable name (e.g., `ssh-mcp-prod-web01-deploy-api`)
|
|
124
|
+
3. A different agent or conversation can recover the session by name
|
|
125
|
+
|
|
126
|
+
**Write a command:**
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"session_id": "a1b2c3d4e5f6",
|
|
131
|
+
"input": "cd /app && git pull\n",
|
|
132
|
+
"wait_seconds": 5
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Always include `\n` to press Enter. Use `\u0003` for Ctrl-C, `\u0004` for Ctrl-D. Set `wait_seconds` high enough for the command to produce output (default: 1 second).
|
|
137
|
+
|
|
138
|
+
**Read more output:**
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"session_id": "a1b2c3d4e5f6",
|
|
143
|
+
"wait_seconds": 10
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Check `pending_output_chars` in the response — if non-zero, call again to drain the buffer.
|
|
148
|
+
|
|
149
|
+
**Session lifecycle:**
|
|
150
|
+
- `ssh_ensure_session` is idempotent — call it at the start of each step
|
|
151
|
+
- Sessions auto-detect dead connections via SSH keepalive (~90 seconds)
|
|
152
|
+
- Response includes `uptime_seconds`, `idle_seconds`, and `exit_reason` for health monitoring
|
|
153
|
+
- Use `auto_close: true` for one-shot commands that should clean up when done
|
|
154
|
+
- Exited sessions are pruned from memory after 1 hour (5 minutes for `auto_close`)
|
|
155
|
+
- `cwd`, `env`, and `shell` only apply when creating a new session — they are ignored when reusing an existing one
|
|
156
|
+
|
|
157
|
+
### File transfer (`ssh_scp`, `ssh_sync`)
|
|
158
|
+
|
|
159
|
+
Copy files between local and remote machines.
|
|
160
|
+
|
|
161
|
+
**scp** — simple file/directory copy:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"target": "prod-web01",
|
|
166
|
+
"direction": "upload",
|
|
167
|
+
"sources": ["/local/path/app.tar.gz"],
|
|
168
|
+
"destination": "/tmp/"
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
For `upload`, `sources` are local paths and `destination` is remote. For `download`, it's reversed. The `target` parameter specifies the host — don't include the host in `sources` or `destination`.
|
|
173
|
+
|
|
174
|
+
**rsync** — incremental sync with `--delete` and `--exclude`:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"target": "prod-web01",
|
|
179
|
+
"direction": "upload",
|
|
180
|
+
"source": "./dist/",
|
|
181
|
+
"destination": "/var/www/app/",
|
|
182
|
+
"delete": true,
|
|
183
|
+
"exclude": ["*.log", ".git"]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Port forwarding (`ssh_forward`)
|
|
188
|
+
|
|
189
|
+
Create local or remote port forwards. This is a separate tool from `ssh_exec` so MCP clients can grant SSH access without granting port forwarding.
|
|
190
|
+
|
|
191
|
+
**Local forward** — make a remote service reachable locally:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"target": "prod-web01",
|
|
196
|
+
"direction": "local",
|
|
197
|
+
"local_port": 15432,
|
|
198
|
+
"remote_host": "prod-db.internal",
|
|
199
|
+
"remote_port": 5432
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This binds `localhost:15432` and tunnels it to `prod-db.internal:5432` through `prod-web01`.
|
|
204
|
+
|
|
205
|
+
**Remote forward** — expose a local service on the remote host:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"target": "prod-web01",
|
|
210
|
+
"direction": "remote",
|
|
211
|
+
"local_port": 3000,
|
|
212
|
+
"remote_host": "localhost",
|
|
213
|
+
"remote_port": 8080
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Forwards bind to `127.0.0.1` by default. Set `bind_address: "0.0.0.0"` to expose on all interfaces (use with caution).
|
|
218
|
+
|
|
219
|
+
Use `ssh_list_forwards` to see active forwards and `ssh_stop_forward` to tear them down.
|
|
220
|
+
|
|
221
|
+
## Watching sessions
|
|
222
|
+
|
|
223
|
+
Every interactive session records a transcript to `~/.local/state/ssh-mcp/<session_id>/transcript.log`.
|
|
224
|
+
|
|
225
|
+
By default, sessions also launch a detached tmux window so you can watch in real time. The tmux session name includes the target and session name for easy identification:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# List ssh-mcp tmux sessions
|
|
229
|
+
tmux ls | grep ssh-mcp
|
|
230
|
+
|
|
231
|
+
# Attach to watch
|
|
232
|
+
tmux attach -t ssh-mcp-prod-web01-deploy-api
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
If you prefer not to use tmux, set `observer_mode: "transcript"` and tail the transcript file directly — the response includes an `observer_command` you can copy-paste.
|
|
236
|
+
|
|
237
|
+
The tmux observer is tied to the session lifecycle: stopping a session closes its tmux window. The MCP server also cleans up tmux on shutdown.
|
|
238
|
+
|
|
239
|
+
## Environment variables
|
|
240
|
+
|
|
241
|
+
| Variable | Default | Description |
|
|
242
|
+
|----------|---------|-------------|
|
|
243
|
+
| `SSH_MCP_SSH_BIN` | `ssh` | Path to the SSH client |
|
|
244
|
+
| `SSH_MCP_SCP_BIN` | `scp` | Path to the SCP client |
|
|
245
|
+
| `SSH_MCP_RSYNC_BIN` | `rsync` | Path to rsync |
|
|
246
|
+
| `SSH_MCP_TMUX_BIN` | `tmux` | Path to tmux |
|
|
247
|
+
| `SSH_MCP_STATE_DIR` | `~/.local/state/ssh-mcp` | Where transcripts are stored |
|
|
248
|
+
|
|
249
|
+
## Security
|
|
250
|
+
|
|
251
|
+
ssh-mcp is designed for single-developer use on your own machine. It runs SSH commands as your user with your credentials.
|
|
252
|
+
|
|
253
|
+
**What's protected:**
|
|
254
|
+
- Port forwarding flags (`-L`, `-R`, `-D`, `-W`) and dangerous SSH options (`ProxyCommand`, `LocalCommand`, `LocalForward`, `RemoteForward`, `DynamicForward`) are blocked in `extra_ssh_args`. The only way to create forwards is through the explicit `ssh_forward` tool, which MCP clients can permission-gate.
|
|
255
|
+
- Transcript files are created with mode `0600` and the state directory with `0700`.
|
|
256
|
+
- All command arguments use `shlex.quote()` to prevent shell injection. Subprocess calls use list arguments, never `shell=True`.
|
|
257
|
+
- Environment variable names are validated against `^[A-Za-z_][A-Za-z0-9_]*$`.
|
|
258
|
+
|
|
259
|
+
**What's not protected:**
|
|
260
|
+
- An agent with `ssh_exec` access can run arbitrary commands on any host your SSH config can reach. The security boundary is SSH itself (keys, known_hosts).
|
|
261
|
+
- Transcript files persist on disk after sessions end and may contain secrets (passwords typed at sudo prompts, API keys in output). Clean up `SSH_MCP_STATE_DIR` when you no longer need them.
|
|
262
|
+
- The `shell` parameter lets agents choose any remote executable. This is by design — the tool is for remote execution.
|
|
263
|
+
|
|
264
|
+
## Known limitations
|
|
265
|
+
|
|
266
|
+
- **POSIX-only remotes** — `cwd`, `env`, and `shell` wrapping assumes a POSIX shell on the remote side. Windows SSH targets need commands written for their shell.
|
|
267
|
+
- **PTY output** — interactive sessions use a PTY, so output includes terminal formatting (ANSI escape codes, command echo, line wrapping). This is intentional — it matches what a human would see.
|
|
268
|
+
- **No multiplexing** — each `ssh_exec` call opens a new SSH connection. If your agent runs many rapid commands to the same host, consider using a session instead, or configure `ControlMaster` in your `~/.ssh/config`.
|
|
269
|
+
- **Transcript growth** — transcripts grow without bound for long-running sessions. The response includes `transcript_size_bytes` so you can monitor this. Restart the session if it gets too large.
|
|
270
|
+
- **Forward connections are standalone** — each `ssh_forward` opens its own SSH connection. Forwards are not tied to sessions.
|
|
271
|
+
|
|
272
|
+
## Tools reference
|
|
273
|
+
|
|
274
|
+
| Tool | Description |
|
|
275
|
+
|------|-------------|
|
|
276
|
+
| `ssh_exec` | Run a one-off remote command |
|
|
277
|
+
| `ssh_scp` | Copy files via scp |
|
|
278
|
+
| `ssh_sync` | Incremental sync via rsync |
|
|
279
|
+
| `ssh_start_session` | Start a new interactive session |
|
|
280
|
+
| `ssh_ensure_session` | Reuse or start an interactive session (recommended) |
|
|
281
|
+
| `ssh_read_session` | Read output from a session |
|
|
282
|
+
| `ssh_write_session` | Write input to a session |
|
|
283
|
+
| `ssh_stop_session` | Stop a session |
|
|
284
|
+
| `ssh_list_sessions` | List tracked sessions |
|
|
285
|
+
| `ssh_forward` | Start a port forward |
|
|
286
|
+
| `ssh_list_forwards` | List tracked forwards |
|
|
287
|
+
| `ssh_stop_forward` | Stop a port forward |
|
|
288
|
+
|
|
289
|
+
All session and forward tools accept standard SSH connection parameters: `port`, `identity_file`, `known_hosts_file`, `strict_host_key_checking`, and `extra_ssh_args`.
|
|
290
|
+
|
|
291
|
+
## Development
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
python3 -m pip install build
|
|
295
|
+
python3 -m unittest discover -s tests -v
|
|
296
|
+
python3 -m compileall src
|
|
297
|
+
python3 -m build
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "slepp-ssh-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A stdio MCP server that exposes Bash-like SSH execution and sessions."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = []
|
|
13
|
+
keywords = ["mcp", "ssh", "scp", "rsync", "tmux"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Operating System :: POSIX",
|
|
21
|
+
"Topic :: System :: Networking",
|
|
22
|
+
"Topic :: System :: Shells",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/slepp/ssh-mcp"
|
|
28
|
+
Repository = "https://github.com/slepp/ssh-mcp"
|
|
29
|
+
Issues = "https://github.com/slepp/ssh-mcp/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
ssh-mcp = "ssh_mcp.__main__:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
package-dir = { "" = "src" }
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|