mcp-remote-ssh 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.
- mcp_remote_ssh-0.1.0/LICENSE +21 -0
- mcp_remote_ssh-0.1.0/PKG-INFO +209 -0
- mcp_remote_ssh-0.1.0/README.md +179 -0
- mcp_remote_ssh-0.1.0/pyproject.toml +61 -0
- mcp_remote_ssh-0.1.0/setup.cfg +4 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh/__init__.py +37 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh/lifespan.py +33 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh/server/__init__.py +9 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh/server/connection.py +97 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh/server/execute.py +93 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh/server/forward.py +183 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh/server/helpers.py +35 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh/server/sftp.py +184 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh/server/shell.py +192 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh/session.py +180 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh.egg-info/PKG-INFO +209 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh.egg-info/SOURCES.txt +19 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh.egg-info/dependency_links.txt +1 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh.egg-info/entry_points.txt +2 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh.egg-info/requires.txt +4 -0
- mcp_remote_ssh-0.1.0/src/mcp_remote_ssh.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohammadfaiz Bawa
|
|
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,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-remote-ssh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for remote SSH operations -- persistent sessions, structured command execution, SFTP file transfer, and port forwarding for AI agents.
|
|
5
|
+
Author: Mohammadfaiz Bawa
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/faizbawa/mcp-remote-ssh
|
|
8
|
+
Project-URL: Repository, https://github.com/faizbawa/mcp-remote-ssh
|
|
9
|
+
Project-URL: Issues, https://github.com/faizbawa/mcp-remote-ssh/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/faizbawa/mcp-remote-ssh/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: mcp,ssh,remote,sftp,paramiko,ai,model-context-protocol,cursor,claude
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: System :: Networking
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: fastmcp<4,>=3.0
|
|
26
|
+
Requires-Dist: paramiko>=3.4
|
|
27
|
+
Requires-Dist: click>=8.0
|
|
28
|
+
Requires-Dist: loguru>=0.7
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# mcp-remote-ssh
|
|
32
|
+
|
|
33
|
+
MCP server for remote SSH operations. Gives AI agents persistent SSH sessions, structured command output, SFTP file transfer, and SSH port forwarding -- with native password and key-based authentication.
|
|
34
|
+
|
|
35
|
+
## Why this exists
|
|
36
|
+
|
|
37
|
+
Existing SSH MCP servers each solve part of the problem but none combine all of:
|
|
38
|
+
|
|
39
|
+
- **Password + key auth** -- connect to any host, whether it uses passwords, SSH keys, or an agent
|
|
40
|
+
- **Dual execution model** -- one-shot `exec_command()` with real exit codes *and* persistent interactive shells for long-running workflows
|
|
41
|
+
- **SFTP** -- read, write, upload, download files properly instead of piping through a PTY
|
|
42
|
+
- **Port forwarding** -- SSH tunnels for accessing remote services (databases, web UIs, VNC, etc.)
|
|
43
|
+
- **Structured output** -- `stdout`, `stderr`, and `exit_code` as separate fields, not screen scrapes
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Via uv (recommended)
|
|
49
|
+
uv tool install mcp-remote-ssh
|
|
50
|
+
|
|
51
|
+
# Via pip
|
|
52
|
+
pip install mcp-remote-ssh
|
|
53
|
+
|
|
54
|
+
# From source
|
|
55
|
+
git clone https://github.com/faizbawa/mcp-remote-ssh.git
|
|
56
|
+
cd mcp-remote-ssh
|
|
57
|
+
uv sync
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
### Cursor / Claude Desktop
|
|
63
|
+
|
|
64
|
+
Add to your MCP client configuration:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"remote-ssh": {
|
|
70
|
+
"command": "uvx",
|
|
71
|
+
"args": ["mcp-remote-ssh"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or if running from source:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"remote-ssh": {
|
|
83
|
+
"command": "uv",
|
|
84
|
+
"args": ["run", "--directory", "/path/to/mcp-remote-ssh", "mcp-remote-ssh"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Tools (18 total)
|
|
91
|
+
|
|
92
|
+
### Connection management
|
|
93
|
+
|
|
94
|
+
| Tool | Description |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `ssh_connect` | Connect to a host (password, key, or agent auth). Returns `session_id`. |
|
|
97
|
+
| `ssh_list_sessions` | List all active sessions with status |
|
|
98
|
+
| `ssh_close_session` | Close a session and release all resources |
|
|
99
|
+
|
|
100
|
+
### Structured execution (one-shot, clean output)
|
|
101
|
+
|
|
102
|
+
| Tool | Description |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `ssh_execute` | Run a command, returns `{stdout, stderr, exit_code}` |
|
|
105
|
+
| `ssh_sudo_execute` | Run a command with sudo elevation |
|
|
106
|
+
|
|
107
|
+
### Interactive shell (persistent state)
|
|
108
|
+
|
|
109
|
+
| Tool | Description |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `ssh_shell_open` | Open a persistent interactive shell (`invoke_shell`) |
|
|
112
|
+
| `ssh_shell_send` | Send text to the shell (with optional Enter) |
|
|
113
|
+
| `ssh_shell_read` | Read current shell buffer (poll for output) |
|
|
114
|
+
| `ssh_shell_send_control` | Send Ctrl+C, Ctrl+D, etc. |
|
|
115
|
+
| `ssh_shell_wait` | Wait for a pattern or output to stabilize |
|
|
116
|
+
|
|
117
|
+
### File transfer (SFTP)
|
|
118
|
+
|
|
119
|
+
| Tool | Description |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `ssh_upload_file` | Upload a local file to the remote host |
|
|
122
|
+
| `ssh_download_file` | Download a remote file to local machine |
|
|
123
|
+
| `ssh_read_remote_file` | Read a text file on the remote host |
|
|
124
|
+
| `ssh_write_remote_file` | Write text to a remote file (create or append) |
|
|
125
|
+
| `ssh_list_remote_dir` | List directory contents with metadata |
|
|
126
|
+
|
|
127
|
+
### Port forwarding
|
|
128
|
+
|
|
129
|
+
| Tool | Description |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `ssh_forward_port` | Create an SSH tunnel (local port -> remote port) |
|
|
132
|
+
| `ssh_list_forwards` | List active port forwards for a session |
|
|
133
|
+
| `ssh_close_forward` | Close a specific port forward |
|
|
134
|
+
|
|
135
|
+
## Quick start
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
# Connect with password
|
|
139
|
+
ssh_connect(host="myserver.example.com", username="admin", password="secret")
|
|
140
|
+
→ {"session_id": "a1b2c3d4", "connected": true, ...}
|
|
141
|
+
|
|
142
|
+
# Run a structured command
|
|
143
|
+
ssh_execute(session_id="a1b2c3d4", command="df -h /")
|
|
144
|
+
→ {"stdout": "Filesystem Size Used ...", "stderr": "", "exit_code": 0}
|
|
145
|
+
|
|
146
|
+
# Open a persistent shell for interactive work
|
|
147
|
+
ssh_shell_open(session_id="a1b2c3d4")
|
|
148
|
+
ssh_shell_send(session_id="a1b2c3d4", data="cd /opt && make -j$(nproc)")
|
|
149
|
+
ssh_shell_wait(session_id="a1b2c3d4", pattern="$ ", timeout=600)
|
|
150
|
+
|
|
151
|
+
# Transfer files via SFTP
|
|
152
|
+
ssh_upload_file(session_id="a1b2c3d4", local_path="config.yaml", remote_path="/etc/app/config.yaml")
|
|
153
|
+
ssh_read_remote_file(session_id="a1b2c3d4", remote_path="/var/log/app.log")
|
|
154
|
+
|
|
155
|
+
# Set up an SSH tunnel
|
|
156
|
+
ssh_forward_port(session_id="a1b2c3d4", remote_port=5432, local_port=15432)
|
|
157
|
+
# Now connect to localhost:15432 to reach the remote PostgreSQL
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Architecture
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
src/mcp_remote_ssh/
|
|
164
|
+
├── __init__.py # CLI entry point (click)
|
|
165
|
+
├── lifespan.py # FastMCP lifespan (session cleanup)
|
|
166
|
+
├── session.py # SSHSession, SessionStore, PortForward
|
|
167
|
+
└── server/
|
|
168
|
+
├── __init__.py # FastMCP app + tool registration
|
|
169
|
+
├── helpers.py # get_session, require_connected, require_shell
|
|
170
|
+
├── connection.py # ssh_connect, ssh_list_sessions, ssh_close_session
|
|
171
|
+
├── execute.py # ssh_execute, ssh_sudo_execute
|
|
172
|
+
├── shell.py # ssh_shell_open/send/read/control/wait
|
|
173
|
+
├── sftp.py # upload, download, read, write, list_dir
|
|
174
|
+
└── forward.py # forward_port, list_forwards, close_forward
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Key design decisions
|
|
178
|
+
|
|
179
|
+
- **[Paramiko](https://www.paramiko.org/)** for SSH -- mature, pure-Python, supports password auth, SFTP, channels, and port forwarding natively
|
|
180
|
+
- **[FastMCP](https://github.com/PrefectHQ/fastmcp)** for MCP protocol
|
|
181
|
+
- **Dual execution model** -- `exec_command()` for structured one-shot commands (returns exit codes), `invoke_shell()` for persistent interactive sessions
|
|
182
|
+
- **Async wrappers** -- all blocking Paramiko calls run in `run_in_executor` to avoid blocking the event loop
|
|
183
|
+
- **Shell buffer management** -- interactive shell keeps a 500KB rolling buffer for `shell_read` polling
|
|
184
|
+
|
|
185
|
+
## Transport options
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# stdio (default, for Cursor/Claude Desktop)
|
|
189
|
+
mcp-remote-ssh
|
|
190
|
+
|
|
191
|
+
# SSE
|
|
192
|
+
mcp-remote-ssh --transport sse --host 0.0.0.0 --port 9810
|
|
193
|
+
|
|
194
|
+
# Streamable HTTP
|
|
195
|
+
mcp-remote-ssh --transport streamable-http --host 0.0.0.0 --port 9810
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Dependencies
|
|
199
|
+
|
|
200
|
+
| Package | Purpose |
|
|
201
|
+
|---|---|
|
|
202
|
+
| [FastMCP](https://github.com/PrefectHQ/fastmcp) | MCP protocol handling |
|
|
203
|
+
| [Paramiko](https://www.paramiko.org/) | SSH2 protocol (connections, channels, SFTP) |
|
|
204
|
+
| [Click](https://click.palletsprojects.com/) | CLI interface |
|
|
205
|
+
| [Loguru](https://loguru.readthedocs.io/) | Logging |
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# mcp-remote-ssh
|
|
2
|
+
|
|
3
|
+
MCP server for remote SSH operations. Gives AI agents persistent SSH sessions, structured command output, SFTP file transfer, and SSH port forwarding -- with native password and key-based authentication.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
Existing SSH MCP servers each solve part of the problem but none combine all of:
|
|
8
|
+
|
|
9
|
+
- **Password + key auth** -- connect to any host, whether it uses passwords, SSH keys, or an agent
|
|
10
|
+
- **Dual execution model** -- one-shot `exec_command()` with real exit codes *and* persistent interactive shells for long-running workflows
|
|
11
|
+
- **SFTP** -- read, write, upload, download files properly instead of piping through a PTY
|
|
12
|
+
- **Port forwarding** -- SSH tunnels for accessing remote services (databases, web UIs, VNC, etc.)
|
|
13
|
+
- **Structured output** -- `stdout`, `stderr`, and `exit_code` as separate fields, not screen scrapes
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Via uv (recommended)
|
|
19
|
+
uv tool install mcp-remote-ssh
|
|
20
|
+
|
|
21
|
+
# Via pip
|
|
22
|
+
pip install mcp-remote-ssh
|
|
23
|
+
|
|
24
|
+
# From source
|
|
25
|
+
git clone https://github.com/faizbawa/mcp-remote-ssh.git
|
|
26
|
+
cd mcp-remote-ssh
|
|
27
|
+
uv sync
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
### Cursor / Claude Desktop
|
|
33
|
+
|
|
34
|
+
Add to your MCP client configuration:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"remote-ssh": {
|
|
40
|
+
"command": "uvx",
|
|
41
|
+
"args": ["mcp-remote-ssh"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or if running from source:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"remote-ssh": {
|
|
53
|
+
"command": "uv",
|
|
54
|
+
"args": ["run", "--directory", "/path/to/mcp-remote-ssh", "mcp-remote-ssh"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Tools (18 total)
|
|
61
|
+
|
|
62
|
+
### Connection management
|
|
63
|
+
|
|
64
|
+
| Tool | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `ssh_connect` | Connect to a host (password, key, or agent auth). Returns `session_id`. |
|
|
67
|
+
| `ssh_list_sessions` | List all active sessions with status |
|
|
68
|
+
| `ssh_close_session` | Close a session and release all resources |
|
|
69
|
+
|
|
70
|
+
### Structured execution (one-shot, clean output)
|
|
71
|
+
|
|
72
|
+
| Tool | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `ssh_execute` | Run a command, returns `{stdout, stderr, exit_code}` |
|
|
75
|
+
| `ssh_sudo_execute` | Run a command with sudo elevation |
|
|
76
|
+
|
|
77
|
+
### Interactive shell (persistent state)
|
|
78
|
+
|
|
79
|
+
| Tool | Description |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `ssh_shell_open` | Open a persistent interactive shell (`invoke_shell`) |
|
|
82
|
+
| `ssh_shell_send` | Send text to the shell (with optional Enter) |
|
|
83
|
+
| `ssh_shell_read` | Read current shell buffer (poll for output) |
|
|
84
|
+
| `ssh_shell_send_control` | Send Ctrl+C, Ctrl+D, etc. |
|
|
85
|
+
| `ssh_shell_wait` | Wait for a pattern or output to stabilize |
|
|
86
|
+
|
|
87
|
+
### File transfer (SFTP)
|
|
88
|
+
|
|
89
|
+
| Tool | Description |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `ssh_upload_file` | Upload a local file to the remote host |
|
|
92
|
+
| `ssh_download_file` | Download a remote file to local machine |
|
|
93
|
+
| `ssh_read_remote_file` | Read a text file on the remote host |
|
|
94
|
+
| `ssh_write_remote_file` | Write text to a remote file (create or append) |
|
|
95
|
+
| `ssh_list_remote_dir` | List directory contents with metadata |
|
|
96
|
+
|
|
97
|
+
### Port forwarding
|
|
98
|
+
|
|
99
|
+
| Tool | Description |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `ssh_forward_port` | Create an SSH tunnel (local port -> remote port) |
|
|
102
|
+
| `ssh_list_forwards` | List active port forwards for a session |
|
|
103
|
+
| `ssh_close_forward` | Close a specific port forward |
|
|
104
|
+
|
|
105
|
+
## Quick start
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
# Connect with password
|
|
109
|
+
ssh_connect(host="myserver.example.com", username="admin", password="secret")
|
|
110
|
+
→ {"session_id": "a1b2c3d4", "connected": true, ...}
|
|
111
|
+
|
|
112
|
+
# Run a structured command
|
|
113
|
+
ssh_execute(session_id="a1b2c3d4", command="df -h /")
|
|
114
|
+
→ {"stdout": "Filesystem Size Used ...", "stderr": "", "exit_code": 0}
|
|
115
|
+
|
|
116
|
+
# Open a persistent shell for interactive work
|
|
117
|
+
ssh_shell_open(session_id="a1b2c3d4")
|
|
118
|
+
ssh_shell_send(session_id="a1b2c3d4", data="cd /opt && make -j$(nproc)")
|
|
119
|
+
ssh_shell_wait(session_id="a1b2c3d4", pattern="$ ", timeout=600)
|
|
120
|
+
|
|
121
|
+
# Transfer files via SFTP
|
|
122
|
+
ssh_upload_file(session_id="a1b2c3d4", local_path="config.yaml", remote_path="/etc/app/config.yaml")
|
|
123
|
+
ssh_read_remote_file(session_id="a1b2c3d4", remote_path="/var/log/app.log")
|
|
124
|
+
|
|
125
|
+
# Set up an SSH tunnel
|
|
126
|
+
ssh_forward_port(session_id="a1b2c3d4", remote_port=5432, local_port=15432)
|
|
127
|
+
# Now connect to localhost:15432 to reach the remote PostgreSQL
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Architecture
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
src/mcp_remote_ssh/
|
|
134
|
+
├── __init__.py # CLI entry point (click)
|
|
135
|
+
├── lifespan.py # FastMCP lifespan (session cleanup)
|
|
136
|
+
├── session.py # SSHSession, SessionStore, PortForward
|
|
137
|
+
└── server/
|
|
138
|
+
├── __init__.py # FastMCP app + tool registration
|
|
139
|
+
├── helpers.py # get_session, require_connected, require_shell
|
|
140
|
+
├── connection.py # ssh_connect, ssh_list_sessions, ssh_close_session
|
|
141
|
+
├── execute.py # ssh_execute, ssh_sudo_execute
|
|
142
|
+
├── shell.py # ssh_shell_open/send/read/control/wait
|
|
143
|
+
├── sftp.py # upload, download, read, write, list_dir
|
|
144
|
+
└── forward.py # forward_port, list_forwards, close_forward
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Key design decisions
|
|
148
|
+
|
|
149
|
+
- **[Paramiko](https://www.paramiko.org/)** for SSH -- mature, pure-Python, supports password auth, SFTP, channels, and port forwarding natively
|
|
150
|
+
- **[FastMCP](https://github.com/PrefectHQ/fastmcp)** for MCP protocol
|
|
151
|
+
- **Dual execution model** -- `exec_command()` for structured one-shot commands (returns exit codes), `invoke_shell()` for persistent interactive sessions
|
|
152
|
+
- **Async wrappers** -- all blocking Paramiko calls run in `run_in_executor` to avoid blocking the event loop
|
|
153
|
+
- **Shell buffer management** -- interactive shell keeps a 500KB rolling buffer for `shell_read` polling
|
|
154
|
+
|
|
155
|
+
## Transport options
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# stdio (default, for Cursor/Claude Desktop)
|
|
159
|
+
mcp-remote-ssh
|
|
160
|
+
|
|
161
|
+
# SSE
|
|
162
|
+
mcp-remote-ssh --transport sse --host 0.0.0.0 --port 9810
|
|
163
|
+
|
|
164
|
+
# Streamable HTTP
|
|
165
|
+
mcp-remote-ssh --transport streamable-http --host 0.0.0.0 --port 9810
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Dependencies
|
|
169
|
+
|
|
170
|
+
| Package | Purpose |
|
|
171
|
+
|---|---|
|
|
172
|
+
| [FastMCP](https://github.com/PrefectHQ/fastmcp) | MCP protocol handling |
|
|
173
|
+
| [Paramiko](https://www.paramiko.org/) | SSH2 protocol (connections, channels, SFTP) |
|
|
174
|
+
| [Click](https://click.palletsprojects.com/) | CLI interface |
|
|
175
|
+
| [Loguru](https://loguru.readthedocs.io/) | Logging |
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-remote-ssh"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for remote SSH operations -- persistent sessions, structured command execution, SFTP file transfer, and port forwarding for AI agents."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
keywords = ["mcp", "ssh", "remote", "sftp", "paramiko", "ai", "model-context-protocol", "cursor", "claude"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Intended Audience :: System Administrators",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: System :: Networking",
|
|
19
|
+
"Topic :: System :: Systems Administration",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"fastmcp>=3.0,<4",
|
|
23
|
+
"paramiko>=3.4",
|
|
24
|
+
"click>=8.0",
|
|
25
|
+
"loguru>=0.7",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[project.authors]]
|
|
29
|
+
name = "Mohammadfaiz Bawa"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/faizbawa/mcp-remote-ssh"
|
|
33
|
+
Repository = "https://github.com/faizbawa/mcp-remote-ssh"
|
|
34
|
+
Issues = "https://github.com/faizbawa/mcp-remote-ssh/issues"
|
|
35
|
+
Changelog = "https://github.com/faizbawa/mcp-remote-ssh/blob/main/CHANGELOG.md"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
mcp-remote-ssh = "mcp_remote_ssh:main"
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=9.0",
|
|
43
|
+
"pytest-asyncio>=1.0",
|
|
44
|
+
"pytest-cov>=7.0",
|
|
45
|
+
"pytest-mock>=3.15",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.uv]
|
|
49
|
+
package = true
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 120
|
|
53
|
+
indent-width = 4
|
|
54
|
+
|
|
55
|
+
lint.select = ["E", "F", "B", "W", "I", "N", "UP", "S", "BLE", "C4", "EM", "ISC"]
|
|
56
|
+
lint.ignore = ["EM102"]
|
|
57
|
+
lint.fixable = ["ALL"]
|
|
58
|
+
lint.per-file-ignores."tests/**/*.py" = ["S101", "S106"]
|
|
59
|
+
|
|
60
|
+
format.quote-style = "single"
|
|
61
|
+
format.indent-style = "space"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
LOG_DIR = Path.home() / '.mcp_remote_ssh'
|
|
10
|
+
LOG_DIR.mkdir(exist_ok=True)
|
|
11
|
+
logger.add(LOG_DIR / 'log.log', rotation='10 MB')
|
|
12
|
+
except Exception as e: # noqa: BLE001
|
|
13
|
+
logger.error(f'Failed to set up logger directory: {e}')
|
|
14
|
+
|
|
15
|
+
if sys.platform == 'win32':
|
|
16
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.command()
|
|
20
|
+
@click.option('--transport', type=click.Choice(['stdio', 'sse', 'streamable-http']), default='stdio')
|
|
21
|
+
@click.option('--host', default='0.0.0.0', help='Host to bind to for SSE or Streamable HTTP transport') # noqa: S104
|
|
22
|
+
@click.option('--port', default=9810, help='Port to listen on for SSE or Streamable HTTP transport')
|
|
23
|
+
def main(
|
|
24
|
+
transport: str,
|
|
25
|
+
host: str,
|
|
26
|
+
port: int,
|
|
27
|
+
) -> None:
|
|
28
|
+
from mcp_remote_ssh.server import mcp
|
|
29
|
+
|
|
30
|
+
if transport == 'stdio':
|
|
31
|
+
asyncio.run(mcp.run_async(transport=transport))
|
|
32
|
+
elif transport in ('sse', 'streamable-http'):
|
|
33
|
+
asyncio.run(mcp.run_async(transport=transport, host=host, port=port))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == '__main__':
|
|
37
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, AsyncIterator
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from mcp_remote_ssh.session import SessionStore
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from fastmcp import FastMCP
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class LifespanContext:
|
|
17
|
+
sessions: SessionStore
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@asynccontextmanager
|
|
21
|
+
async def lifespan(app: FastMCP) -> AsyncIterator[LifespanContext]:
|
|
22
|
+
ctx = LifespanContext(sessions=SessionStore())
|
|
23
|
+
try:
|
|
24
|
+
yield ctx
|
|
25
|
+
finally:
|
|
26
|
+
for info in ctx.sessions.list_all():
|
|
27
|
+
sid = info['session_id']
|
|
28
|
+
session = ctx.sessions.get(sid)
|
|
29
|
+
errors = session.close()
|
|
30
|
+
if errors:
|
|
31
|
+
logger.warning(f'Session {sid} cleanup errors: {errors}')
|
|
32
|
+
else:
|
|
33
|
+
logger.debug(f'Cleaned up session {sid}')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from fastmcp import FastMCP
|
|
2
|
+
|
|
3
|
+
from mcp_remote_ssh.lifespan import LifespanContext, lifespan
|
|
4
|
+
|
|
5
|
+
__all__ = ['mcp']
|
|
6
|
+
|
|
7
|
+
mcp = FastMCP('mcp-remote-ssh', lifespan=lifespan)
|
|
8
|
+
|
|
9
|
+
from mcp_remote_ssh.server import connection, execute, forward, sftp, shell # noqa: E402, F401
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import paramiko
|
|
7
|
+
from fastmcp import Context
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from mcp_remote_ssh.server import mcp
|
|
11
|
+
from mcp_remote_ssh.server.helpers import get_session, get_store
|
|
12
|
+
from mcp_remote_ssh.session import SSHSession
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@mcp.tool()
|
|
16
|
+
async def ssh_connect(
|
|
17
|
+
ctx: Context,
|
|
18
|
+
host: str,
|
|
19
|
+
username: str = 'root',
|
|
20
|
+
password: str = '',
|
|
21
|
+
key_path: str = '',
|
|
22
|
+
port: int = 22,
|
|
23
|
+
timeout: int = 60,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Connect to a remote host via SSH. Returns a session_id for use with all
|
|
26
|
+
other tools. Supports password and key-based authentication.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
host: Hostname or IP address of the remote server.
|
|
30
|
+
username: SSH username (default: root).
|
|
31
|
+
password: Password for authentication. Leave empty for key-based auth.
|
|
32
|
+
key_path: Path to SSH private key file. Leave empty for password auth.
|
|
33
|
+
port: SSH port (default: 22).
|
|
34
|
+
timeout: Connection timeout in seconds (default: 60).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Session info dict with session_id, host, and connection status.
|
|
38
|
+
"""
|
|
39
|
+
store = get_store(ctx)
|
|
40
|
+
session = SSHSession(host=host, username=username, port=port)
|
|
41
|
+
session.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # noqa: S507
|
|
42
|
+
|
|
43
|
+
connect_kwargs: dict[str, Any] = {
|
|
44
|
+
'hostname': host,
|
|
45
|
+
'port': port,
|
|
46
|
+
'username': username,
|
|
47
|
+
'timeout': timeout,
|
|
48
|
+
'allow_agent': False,
|
|
49
|
+
'look_for_keys': False,
|
|
50
|
+
}
|
|
51
|
+
if key_path:
|
|
52
|
+
connect_kwargs['key_filename'] = key_path
|
|
53
|
+
connect_kwargs['look_for_keys'] = True
|
|
54
|
+
elif password:
|
|
55
|
+
connect_kwargs['password'] = password
|
|
56
|
+
else:
|
|
57
|
+
connect_kwargs['allow_agent'] = True
|
|
58
|
+
connect_kwargs['look_for_keys'] = True
|
|
59
|
+
|
|
60
|
+
loop = asyncio.get_running_loop()
|
|
61
|
+
await loop.run_in_executor(None, lambda: session.client.connect(**connect_kwargs))
|
|
62
|
+
|
|
63
|
+
await store.add(session)
|
|
64
|
+
logger.info(f'Connected to {username}@{host}:{port} as session {session.session_id}')
|
|
65
|
+
return session.summary()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
async def ssh_list_sessions(ctx: Context) -> list[dict[str, Any]]:
|
|
70
|
+
"""List all active SSH sessions with their connection status and details.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of session info dicts.
|
|
74
|
+
"""
|
|
75
|
+
store = get_store(ctx)
|
|
76
|
+
return store.list_all()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@mcp.tool()
|
|
80
|
+
async def ssh_close_session(ctx: Context, session_id: str) -> str:
|
|
81
|
+
"""Close an SSH session and release all its resources (shell, SFTP,
|
|
82
|
+
port forwards). WARNING: this kills any running processes in the session.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
session_id: The session ID returned by ssh_connect.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Confirmation message.
|
|
89
|
+
"""
|
|
90
|
+
store = get_store(ctx)
|
|
91
|
+
session = get_session(ctx, session_id)
|
|
92
|
+
errors = session.close()
|
|
93
|
+
await store.remove(session_id)
|
|
94
|
+
logger.info(f'Closed session {session_id} to {session.host}')
|
|
95
|
+
if errors:
|
|
96
|
+
return f'Session {session_id} closed with warnings: {"; ".join(errors)}'
|
|
97
|
+
return f'Session {session_id} to {session.host} closed.'
|