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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+