mcp-remote-ssh 0.2.2__tar.gz → 0.3.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.2.2/src/mcp_remote_ssh.egg-info → mcp_remote_ssh-0.3.0}/PKG-INFO +7 -6
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/README.md +6 -5
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/pyproject.toml +1 -1
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/server/execute.py +25 -15
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0/src/mcp_remote_ssh.egg-info}/PKG-INFO +7 -6
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/tests/test_edge_cases.py +14 -15
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/LICENSE +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/setup.cfg +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/__init__.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/lifespan.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/server/__init__.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/server/connection.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/server/forward.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/server/helpers.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/server/secrets.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/server/sftp.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/server/shell.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh/session.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh.egg-info/SOURCES.txt +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh.egg-info/dependency_links.txt +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh.egg-info/entry_points.txt +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh.egg-info/requires.txt +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh.egg-info/top_level.txt +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/tests/test_redaction.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/tests/test_secrets_integration.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/tests/test_secrets_load.py +0 -0
- {mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/tests/test_secrets_parsing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-remote-ssh
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: MCP server for remote SSH operations -- persistent sessions, structured command execution, SFTP file transfer, and port forwarding for AI agents.
|
|
5
5
|
Author: Mohammadfaiz Bawa
|
|
6
6
|
License-Expression: MIT
|
|
@@ -84,17 +84,17 @@ ssh_execute(session_id="abc", command="uname -a")
|
|
|
84
84
|
|
|
85
85
|
1. **Local file read** -- the env file lives on your machine, never on the remote host
|
|
86
86
|
2. **Shell injection via builtins** -- uses `read -r VAR <<< 'value' && export VAR` (no process tree exposure)
|
|
87
|
-
3. **
|
|
88
|
-
4. **
|
|
89
|
-
5. **
|
|
87
|
+
3. **Stdin-based exec injection** -- `ssh_execute` feeds secrets via stdin to a bash wrapper, so they never appear in `/proc/*/cmdline`
|
|
88
|
+
4. **Automatic redaction** -- every tool response (`ssh_execute`, `ssh_shell_send`, `ssh_shell_read`, `ssh_read_remote_file`) is scrubbed before reaching the LLM
|
|
89
|
+
5. **Longest-first matching** -- prevents partial-match corruption (e.g., `abc123` is replaced before `abc`)
|
|
90
90
|
|
|
91
91
|
### Security properties
|
|
92
92
|
|
|
93
93
|
| Threat | Mitigated? | How |
|
|
94
94
|
|--------|-----------|-----|
|
|
95
95
|
| Secret in LLM context window | Yes | Output redaction replaces values with `***` |
|
|
96
|
-
| Secret in remote process tree | Yes | Shell builtins (`read`/`export`) don't fork |
|
|
97
|
-
| Secret in
|
|
96
|
+
| Secret in remote process tree (shell) | Yes | Shell builtins (`read`/`export`) don't fork |
|
|
97
|
+
| Secret in remote process tree (exec) | Yes | Secrets fed via stdin, never in `/proc/*/cmdline` |
|
|
98
98
|
| LLM tries `cat` on the env file | N/A | File is local-only, doesn't exist on remote |
|
|
99
99
|
| LLM tries `echo $VAR` | Yes | Output is redacted |
|
|
100
100
|
| Encoded/transformed secret (base64) | No | Only literal matches are redacted |
|
|
@@ -207,6 +207,7 @@ ssh_forward_port(session_id="a1b2c3d4", remote_port=5432, local_port=15432)
|
|
|
207
207
|
Built on **[Paramiko](https://www.paramiko.org/)** (SSH) + **[FastMCP](https://github.com/PrefectHQ/fastmcp)** (MCP protocol).
|
|
208
208
|
|
|
209
209
|
- `ssh_execute` uses `exec_command()` for clean structured output with real exit codes
|
|
210
|
+
- When secrets are loaded, `ssh_execute` feeds exports via stdin to a `bash` wrapper, then `exec`s the actual command -- secrets never appear in the process tree
|
|
210
211
|
- `ssh_shell_*` uses `invoke_shell()` for persistent interactive sessions
|
|
211
212
|
- All blocking Paramiko calls run in `run_in_executor` to stay async
|
|
212
213
|
- Shell keeps a 500KB rolling buffer for `shell_read` polling
|
|
@@ -54,17 +54,17 @@ ssh_execute(session_id="abc", command="uname -a")
|
|
|
54
54
|
|
|
55
55
|
1. **Local file read** -- the env file lives on your machine, never on the remote host
|
|
56
56
|
2. **Shell injection via builtins** -- uses `read -r VAR <<< 'value' && export VAR` (no process tree exposure)
|
|
57
|
-
3. **
|
|
58
|
-
4. **
|
|
59
|
-
5. **
|
|
57
|
+
3. **Stdin-based exec injection** -- `ssh_execute` feeds secrets via stdin to a bash wrapper, so they never appear in `/proc/*/cmdline`
|
|
58
|
+
4. **Automatic redaction** -- every tool response (`ssh_execute`, `ssh_shell_send`, `ssh_shell_read`, `ssh_read_remote_file`) is scrubbed before reaching the LLM
|
|
59
|
+
5. **Longest-first matching** -- prevents partial-match corruption (e.g., `abc123` is replaced before `abc`)
|
|
60
60
|
|
|
61
61
|
### Security properties
|
|
62
62
|
|
|
63
63
|
| Threat | Mitigated? | How |
|
|
64
64
|
|--------|-----------|-----|
|
|
65
65
|
| Secret in LLM context window | Yes | Output redaction replaces values with `***` |
|
|
66
|
-
| Secret in remote process tree | Yes | Shell builtins (`read`/`export`) don't fork |
|
|
67
|
-
| Secret in
|
|
66
|
+
| Secret in remote process tree (shell) | Yes | Shell builtins (`read`/`export`) don't fork |
|
|
67
|
+
| Secret in remote process tree (exec) | Yes | Secrets fed via stdin, never in `/proc/*/cmdline` |
|
|
68
68
|
| LLM tries `cat` on the env file | N/A | File is local-only, doesn't exist on remote |
|
|
69
69
|
| LLM tries `echo $VAR` | Yes | Output is redacted |
|
|
70
70
|
| Encoded/transformed secret (base64) | No | Only literal matches are redacted |
|
|
@@ -177,6 +177,7 @@ ssh_forward_port(session_id="a1b2c3d4", remote_port=5432, local_port=15432)
|
|
|
177
177
|
Built on **[Paramiko](https://www.paramiko.org/)** (SSH) + **[FastMCP](https://github.com/PrefectHQ/fastmcp)** (MCP protocol).
|
|
178
178
|
|
|
179
179
|
- `ssh_execute` uses `exec_command()` for clean structured output with real exit codes
|
|
180
|
+
- When secrets are loaded, `ssh_execute` feeds exports via stdin to a `bash` wrapper, then `exec`s the actual command -- secrets never appear in the process tree
|
|
180
181
|
- `ssh_shell_*` uses `invoke_shell()` for persistent interactive sessions
|
|
181
182
|
- All blocking Paramiko calls run in `run_in_executor` to stay async
|
|
182
183
|
- Shell keeps a 500KB rolling buffer for `shell_read` polling
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mcp-remote-ssh"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "MCP server for remote SSH operations -- persistent sessions, structured command execution, SFTP file transfer, and port forwarding for AI agents."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -11,16 +11,15 @@ from mcp_remote_ssh.server import mcp
|
|
|
11
11
|
from mcp_remote_ssh.server.helpers import get_session, require_connected
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
"""Build
|
|
16
|
-
|
|
17
|
-
since the whole thing runs under a single 'bash -c' invocation."""
|
|
14
|
+
def _build_env_script(session) -> str:
|
|
15
|
+
"""Build export statements for secrets, to be fed via stdin.
|
|
16
|
+
Never appears in /proc/*/cmdline."""
|
|
18
17
|
if not session._secrets:
|
|
19
18
|
return ''
|
|
20
|
-
|
|
19
|
+
lines = []
|
|
21
20
|
for key, value in session._secrets.items():
|
|
22
|
-
|
|
23
|
-
return '
|
|
21
|
+
lines.append(f'export {key}={shlex.quote(value)}')
|
|
22
|
+
return '\n'.join(lines) + '\n'
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
@mcp.tool()
|
|
@@ -48,13 +47,18 @@ async def ssh_execute(
|
|
|
48
47
|
session = get_session(ctx, session_id)
|
|
49
48
|
require_connected(session)
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
full_command = env_prefix + command
|
|
53
|
-
|
|
50
|
+
env_script = _build_env_script(session)
|
|
54
51
|
loop = asyncio.get_running_loop()
|
|
55
52
|
|
|
56
53
|
def _exec() -> dict[str, Any]:
|
|
57
|
-
|
|
54
|
+
if env_script:
|
|
55
|
+
# Feed secrets via stdin so they never appear in /proc/*/cmdline
|
|
56
|
+
script = env_script + f'exec bash -c {shlex.quote(command)}\n'
|
|
57
|
+
stdin_ch, stdout_ch, stderr_ch = session.client.exec_command('bash', timeout=timeout)
|
|
58
|
+
stdin_ch.write(script)
|
|
59
|
+
stdin_ch.channel.shutdown_write()
|
|
60
|
+
else:
|
|
61
|
+
_, stdout_ch, stderr_ch = session.client.exec_command(command, timeout=timeout)
|
|
58
62
|
stdout = stdout_ch.read().decode(errors='replace')
|
|
59
63
|
stderr = stderr_ch.read().decode(errors='replace')
|
|
60
64
|
exit_code = stdout_ch.channel.recv_exit_status()
|
|
@@ -93,17 +97,23 @@ async def ssh_sudo_execute(
|
|
|
93
97
|
session = get_session(ctx, session_id)
|
|
94
98
|
require_connected(session)
|
|
95
99
|
|
|
96
|
-
|
|
100
|
+
env_script = _build_env_script(session)
|
|
97
101
|
|
|
98
102
|
if sudo_password:
|
|
99
|
-
|
|
103
|
+
actual_cmd = f'echo {_shell_quote(sudo_password)} | sudo -SE {command}'
|
|
100
104
|
else:
|
|
101
|
-
|
|
105
|
+
actual_cmd = f'sudo -E {command}'
|
|
102
106
|
|
|
103
107
|
loop = asyncio.get_running_loop()
|
|
104
108
|
|
|
105
109
|
def _exec() -> dict[str, Any]:
|
|
106
|
-
|
|
110
|
+
if env_script:
|
|
111
|
+
script = env_script + f'exec bash -c {shlex.quote(actual_cmd)}\n'
|
|
112
|
+
stdin_ch, stdout_ch, stderr_ch = session.client.exec_command('bash', timeout=timeout)
|
|
113
|
+
stdin_ch.write(script)
|
|
114
|
+
stdin_ch.channel.shutdown_write()
|
|
115
|
+
else:
|
|
116
|
+
_, stdout_ch, stderr_ch = session.client.exec_command(actual_cmd, timeout=timeout)
|
|
107
117
|
stdout = stdout_ch.read().decode(errors='replace')
|
|
108
118
|
stderr = stderr_ch.read().decode(errors='replace')
|
|
109
119
|
exit_code = stdout_ch.channel.recv_exit_status()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-remote-ssh
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: MCP server for remote SSH operations -- persistent sessions, structured command execution, SFTP file transfer, and port forwarding for AI agents.
|
|
5
5
|
Author: Mohammadfaiz Bawa
|
|
6
6
|
License-Expression: MIT
|
|
@@ -84,17 +84,17 @@ ssh_execute(session_id="abc", command="uname -a")
|
|
|
84
84
|
|
|
85
85
|
1. **Local file read** -- the env file lives on your machine, never on the remote host
|
|
86
86
|
2. **Shell injection via builtins** -- uses `read -r VAR <<< 'value' && export VAR` (no process tree exposure)
|
|
87
|
-
3. **
|
|
88
|
-
4. **
|
|
89
|
-
5. **
|
|
87
|
+
3. **Stdin-based exec injection** -- `ssh_execute` feeds secrets via stdin to a bash wrapper, so they never appear in `/proc/*/cmdline`
|
|
88
|
+
4. **Automatic redaction** -- every tool response (`ssh_execute`, `ssh_shell_send`, `ssh_shell_read`, `ssh_read_remote_file`) is scrubbed before reaching the LLM
|
|
89
|
+
5. **Longest-first matching** -- prevents partial-match corruption (e.g., `abc123` is replaced before `abc`)
|
|
90
90
|
|
|
91
91
|
### Security properties
|
|
92
92
|
|
|
93
93
|
| Threat | Mitigated? | How |
|
|
94
94
|
|--------|-----------|-----|
|
|
95
95
|
| Secret in LLM context window | Yes | Output redaction replaces values with `***` |
|
|
96
|
-
| Secret in remote process tree | Yes | Shell builtins (`read`/`export`) don't fork |
|
|
97
|
-
| Secret in
|
|
96
|
+
| Secret in remote process tree (shell) | Yes | Shell builtins (`read`/`export`) don't fork |
|
|
97
|
+
| Secret in remote process tree (exec) | Yes | Secrets fed via stdin, never in `/proc/*/cmdline` |
|
|
98
98
|
| LLM tries `cat` on the env file | N/A | File is local-only, doesn't exist on remote |
|
|
99
99
|
| LLM tries `echo $VAR` | Yes | Output is redacted |
|
|
100
100
|
| Encoded/transformed secret (base64) | No | Only literal matches are redacted |
|
|
@@ -207,6 +207,7 @@ ssh_forward_port(session_id="a1b2c3d4", remote_port=5432, local_port=15432)
|
|
|
207
207
|
Built on **[Paramiko](https://www.paramiko.org/)** (SSH) + **[FastMCP](https://github.com/PrefectHQ/fastmcp)** (MCP protocol).
|
|
208
208
|
|
|
209
209
|
- `ssh_execute` uses `exec_command()` for clean structured output with real exit codes
|
|
210
|
+
- When secrets are loaded, `ssh_execute` feeds exports via stdin to a `bash` wrapper, then `exec`s the actual command -- secrets never appear in the process tree
|
|
210
211
|
- `ssh_shell_*` uses `invoke_shell()` for persistent interactive sessions
|
|
211
212
|
- All blocking Paramiko calls run in `run_in_executor` to stay async
|
|
212
213
|
- Shell keeps a 500KB rolling buffer for `shell_read` polling
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
6
|
from mcp_remote_ssh.session import SSHSession
|
|
7
|
-
from mcp_remote_ssh.server.execute import
|
|
7
|
+
from mcp_remote_ssh.server.execute import _build_env_script
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class TestLongestFirstRedaction:
|
|
@@ -44,35 +44,34 @@ class TestLongestFirstRedaction:
|
|
|
44
44
|
assert redacted == 'a=*** b=***'
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
class
|
|
48
|
-
"""Verify
|
|
47
|
+
class TestEnvScriptForExec:
|
|
48
|
+
"""Verify _build_env_script generates correct export statements."""
|
|
49
49
|
|
|
50
50
|
def test_empty_when_no_secrets(self):
|
|
51
51
|
session = SSHSession(host='test-host')
|
|
52
|
-
assert
|
|
52
|
+
assert _build_env_script(session) == ''
|
|
53
53
|
|
|
54
54
|
def test_single_secret(self):
|
|
55
55
|
session = SSHSession(host='test-host')
|
|
56
56
|
session._secrets = {'TOKEN': 'abc123'}
|
|
57
|
-
|
|
58
|
-
assert 'export TOKEN=abc123' in
|
|
59
|
-
assert
|
|
57
|
+
script = _build_env_script(session)
|
|
58
|
+
assert 'export TOKEN=abc123' in script
|
|
59
|
+
assert script.endswith('\n')
|
|
60
60
|
|
|
61
61
|
def test_multiple_secrets(self):
|
|
62
62
|
session = SSHSession(host='test-host')
|
|
63
63
|
session._secrets = {'A': 'val1', 'B': 'val2'}
|
|
64
|
-
|
|
65
|
-
assert 'export A=val1' in
|
|
66
|
-
assert 'export B=val2' in
|
|
67
|
-
assert
|
|
64
|
+
script = _build_env_script(session)
|
|
65
|
+
assert 'export A=val1' in script
|
|
66
|
+
assert 'export B=val2' in script
|
|
67
|
+
assert '\n' in script
|
|
68
68
|
|
|
69
69
|
def test_quotes_special_characters(self):
|
|
70
70
|
session = SSHSession(host='test-host')
|
|
71
71
|
session._secrets = {'PASS': "it's a p@ss!"}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
assert
|
|
75
|
-
assert "p@ss!" in prefix
|
|
72
|
+
script = _build_env_script(session)
|
|
73
|
+
assert 'PASS=' in script
|
|
74
|
+
assert "p@ss!" in script
|
|
76
75
|
|
|
77
76
|
|
|
78
77
|
class TestShellInjectionSafety:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_remote_ssh-0.2.2 → mcp_remote_ssh-0.3.0}/src/mcp_remote_ssh.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|