mcp-ssh-wrapper 0.1.1__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_ssh_wrapper-0.1.1/.github/workflows/publish-pypi.yml +71 -0
- mcp_ssh_wrapper-0.1.1/.gitignore +8 -0
- mcp_ssh_wrapper-0.1.1/LICENSE +21 -0
- mcp_ssh_wrapper-0.1.1/PKG-INFO +208 -0
- mcp_ssh_wrapper-0.1.1/README.md +187 -0
- mcp_ssh_wrapper-0.1.1/pyproject.toml +38 -0
- mcp_ssh_wrapper-0.1.1/run_server.sh +15 -0
- mcp_ssh_wrapper-0.1.1/run_test.sh +7 -0
- mcp_ssh_wrapper-0.1.1/scripts/test_http_client.py +86 -0
- mcp_ssh_wrapper-0.1.1/src/mcp_ssh_wrapper/__init__.py +3 -0
- mcp_ssh_wrapper-0.1.1/src/mcp_ssh_wrapper/__main__.py +5 -0
- mcp_ssh_wrapper-0.1.1/src/mcp_ssh_wrapper/server.py +90 -0
- mcp_ssh_wrapper-0.1.1/src/mcp_ssh_wrapper/ssh_wrapper.py +54 -0
- mcp_ssh_wrapper-0.1.1/tests/test_ssh_wrapper.py +54 -0
- mcp_ssh_wrapper-0.1.1/uv.lock +747 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
name: Build distributions
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
outputs:
|
|
15
|
+
version: ${{ steps.version.outputs.version }}
|
|
16
|
+
steps:
|
|
17
|
+
- name: Check out repository
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
|
|
22
|
+
- name: Ensure tag points to main
|
|
23
|
+
run: |
|
|
24
|
+
git fetch origin main --depth=1
|
|
25
|
+
git merge-base --is-ancestor "$GITHUB_SHA" "origin/main"
|
|
26
|
+
|
|
27
|
+
- name: Derive version from tag
|
|
28
|
+
id: version
|
|
29
|
+
run: |
|
|
30
|
+
version="${GITHUB_REF_NAME#v}"
|
|
31
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
32
|
+
|
|
33
|
+
- name: Set up Python
|
|
34
|
+
uses: actions/setup-python@v5
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.12"
|
|
37
|
+
|
|
38
|
+
- name: Set up uv
|
|
39
|
+
uses: astral-sh/setup-uv@v4
|
|
40
|
+
|
|
41
|
+
- name: Verify tag matches package version
|
|
42
|
+
run: |
|
|
43
|
+
package_version="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"
|
|
44
|
+
test "$package_version" = "${{ steps.version.outputs.version }}"
|
|
45
|
+
|
|
46
|
+
- name: Build package
|
|
47
|
+
run: uv build
|
|
48
|
+
|
|
49
|
+
- name: Upload distributions
|
|
50
|
+
uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: python-package-distributions
|
|
53
|
+
path: dist/
|
|
54
|
+
|
|
55
|
+
publish:
|
|
56
|
+
name: Publish to PyPI
|
|
57
|
+
needs: build
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
environment:
|
|
60
|
+
name: pypi
|
|
61
|
+
permissions:
|
|
62
|
+
id-token: write
|
|
63
|
+
steps:
|
|
64
|
+
- name: Download distributions
|
|
65
|
+
uses: actions/download-artifact@v4
|
|
66
|
+
with:
|
|
67
|
+
name: python-package-distributions
|
|
68
|
+
path: dist/
|
|
69
|
+
|
|
70
|
+
- name: Publish package to PyPI
|
|
71
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Megascope
|
|
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,208 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-ssh-wrapper
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: An MCP server that executes remote commands through the host ssh binary.
|
|
5
|
+
Author: megascope
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: mcp,server,ssh
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Internet
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: mcp>=1.0.0
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# mcp-ssh-wrapper
|
|
23
|
+
|
|
24
|
+
`mcp-ssh-wrapper` is a minimal MCP server that executes commands on remote hosts by delegating to the local `ssh` binary.
|
|
25
|
+
|
|
26
|
+
The server does not implement SSH authentication. It relies entirely on the host machine's existing SSH configuration, agent, identities, and host key policy.
|
|
27
|
+
|
|
28
|
+
This project runs as a streamable HTTP MCP server. It is intended to be started as a long-lived local process and then connected to by tools like Codex or Claude.
|
|
29
|
+
|
|
30
|
+
The SSH execution path is non-interactive. The server runs `ssh` in batch mode and does not allow prompts on stdin, so hosts must already be reachable using existing SSH config, keys, agent state, and known-hosts entries.
|
|
31
|
+
|
|
32
|
+
Warning: binding this server to anything other than `localhost` or `127.0.0.1` exposes a tool that can use the local machine's SSH configuration, agent, and keys over the network. Treat non-local binding as a high-risk configuration.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- Exposes a single MCP tool: `execute_command`
|
|
37
|
+
- Uses the system `ssh` command directly
|
|
38
|
+
- Returns `stdout`, `stderr`, and `exit_code`
|
|
39
|
+
- Supports an optional execution timeout
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install mcp-ssh-wrapper
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Running the server
|
|
48
|
+
|
|
49
|
+
After installation, run the server with:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
mcp-ssh-wrapper --host 127.0.0.1 --port 8000 --mount-path /mcp
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This exposes the MCP endpoint at `http://127.0.0.1:8000/mcp`.
|
|
56
|
+
|
|
57
|
+
You can also use the module entrypoint:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python -m mcp_ssh_wrapper --host 127.0.0.1 --port 8000 --mount-path /mcp
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Keep this bound to localhost unless you fully understand the security implications.
|
|
64
|
+
|
|
65
|
+
### Local execution
|
|
66
|
+
|
|
67
|
+
For local development from this repository, use the helper script:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
./run_server.sh --host 127.0.0.1 --port 8000 --mount-path /mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If your SSH setup depends on `ssh-agent`, `run_server.sh` will try to recover `SSH_AUTH_SOCK` automatically on macOS.
|
|
74
|
+
|
|
75
|
+
## Add to Codex
|
|
76
|
+
|
|
77
|
+
Start the server first:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
mcp-ssh-wrapper --host 127.0.0.1 --port 8000 --mount-path /mcp
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then register the running HTTP endpoint with Codex:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
codex mcp add mcp-ssh --url http://127.0.0.1:8000/mcp
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Verify the registration:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
codex mcp list
|
|
93
|
+
codex mcp get mcp-ssh
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Codex will connect to the running HTTP endpoint. It does not launch the server process for you in this setup.
|
|
97
|
+
|
|
98
|
+
## Add to Claude
|
|
99
|
+
|
|
100
|
+
### Claude Code CLI
|
|
101
|
+
|
|
102
|
+
Start the server first, then register the URL with Claude Code:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
claude mcp add mcp-ssh --transport http http://127.0.0.1:8000/mcp
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Verify the registration:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
claude mcp list
|
|
112
|
+
claude mcp get mcp-ssh
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Claude Desktop
|
|
116
|
+
|
|
117
|
+
Add this server entry to your `claude_desktop_config.json`:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"mcpServers": {
|
|
122
|
+
"mcp-ssh": {
|
|
123
|
+
"url": "http://127.0.0.1:8000/mcp"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Claude Desktop will connect to the running local HTTP server.
|
|
130
|
+
|
|
131
|
+
## Important limitation
|
|
132
|
+
|
|
133
|
+
`run_test.sh` does not launch the server. Start `mcp-ssh-wrapper ...` or `./run_server.sh ...` first, then run the test wrapper against the live endpoint.
|
|
134
|
+
|
|
135
|
+
## Local smoke test
|
|
136
|
+
|
|
137
|
+
To verify startup, MCP initialization, and tool discovery against a running server:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
./run_test.sh --skip-call
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
To target a non-default endpoint:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
./run_test.sh --url http://127.0.0.1:8000/mcp --skip-call
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
To also invoke the SSH tool:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
./run_test.sh --host my-host --command "uname -a"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Tool
|
|
156
|
+
|
|
157
|
+
### `execute_command`
|
|
158
|
+
|
|
159
|
+
Arguments:
|
|
160
|
+
|
|
161
|
+
- `host`: SSH host or `user@host`, resolved by the local SSH client
|
|
162
|
+
- `command`: Command string to execute remotely
|
|
163
|
+
- `timeout_seconds`: Optional timeout. `0` disables timeout handling.
|
|
164
|
+
|
|
165
|
+
Result:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"host": "prod-box",
|
|
170
|
+
"command": "uname -a",
|
|
171
|
+
"stdout": "Linux ...\n",
|
|
172
|
+
"stderr": "",
|
|
173
|
+
"exit_code": 0
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Notes
|
|
178
|
+
|
|
179
|
+
- SSH options should be configured in `~/.ssh/config` or the host environment.
|
|
180
|
+
- This server invokes `ssh -o BatchMode=yes`, so password prompts and interactive confirmations are disabled.
|
|
181
|
+
- Authentication, host key policy, and identity selection are still handled by the local `ssh` client and its existing configuration.
|
|
182
|
+
|
|
183
|
+
## Releasing
|
|
184
|
+
|
|
185
|
+
This project is configured to publish to PyPI using GitHub Trusted Publishing via [.github/workflows/publish-pypi.yml](mcp-ssh-wrapper/.github/workflows/publish-pypi.yml).
|
|
186
|
+
|
|
187
|
+
Release flow:
|
|
188
|
+
|
|
189
|
+
1. Update `project.version` in [pyproject.toml](mcp-ssh-wrapper/pyproject.toml).
|
|
190
|
+
2. Commit the version bump and push it to `main`.
|
|
191
|
+
3. Tag the `main` commit with a matching version tag in the form `vX.Y.Z`.
|
|
192
|
+
4. Push the tag to GitHub.
|
|
193
|
+
|
|
194
|
+
Example for version `0.1.0`:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
git checkout main
|
|
198
|
+
git pull
|
|
199
|
+
git tag v0.1.0
|
|
200
|
+
git push origin v0.1.0
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The GitHub Actions workflow will:
|
|
204
|
+
|
|
205
|
+
- verify the tag points to a commit on `main`
|
|
206
|
+
- verify the tag matches `project.version`
|
|
207
|
+
- build the package
|
|
208
|
+
- publish it to PyPI using the GitHub `pypi` environment
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# mcp-ssh-wrapper
|
|
2
|
+
|
|
3
|
+
`mcp-ssh-wrapper` is a minimal MCP server that executes commands on remote hosts by delegating to the local `ssh` binary.
|
|
4
|
+
|
|
5
|
+
The server does not implement SSH authentication. It relies entirely on the host machine's existing SSH configuration, agent, identities, and host key policy.
|
|
6
|
+
|
|
7
|
+
This project runs as a streamable HTTP MCP server. It is intended to be started as a long-lived local process and then connected to by tools like Codex or Claude.
|
|
8
|
+
|
|
9
|
+
The SSH execution path is non-interactive. The server runs `ssh` in batch mode and does not allow prompts on stdin, so hosts must already be reachable using existing SSH config, keys, agent state, and known-hosts entries.
|
|
10
|
+
|
|
11
|
+
Warning: binding this server to anything other than `localhost` or `127.0.0.1` exposes a tool that can use the local machine's SSH configuration, agent, and keys over the network. Treat non-local binding as a high-risk configuration.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Exposes a single MCP tool: `execute_command`
|
|
16
|
+
- Uses the system `ssh` command directly
|
|
17
|
+
- Returns `stdout`, `stderr`, and `exit_code`
|
|
18
|
+
- Supports an optional execution timeout
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install mcp-ssh-wrapper
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Running the server
|
|
27
|
+
|
|
28
|
+
After installation, run the server with:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
mcp-ssh-wrapper --host 127.0.0.1 --port 8000 --mount-path /mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This exposes the MCP endpoint at `http://127.0.0.1:8000/mcp`.
|
|
35
|
+
|
|
36
|
+
You can also use the module entrypoint:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m mcp_ssh_wrapper --host 127.0.0.1 --port 8000 --mount-path /mcp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Keep this bound to localhost unless you fully understand the security implications.
|
|
43
|
+
|
|
44
|
+
### Local execution
|
|
45
|
+
|
|
46
|
+
For local development from this repository, use the helper script:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
./run_server.sh --host 127.0.0.1 --port 8000 --mount-path /mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If your SSH setup depends on `ssh-agent`, `run_server.sh` will try to recover `SSH_AUTH_SOCK` automatically on macOS.
|
|
53
|
+
|
|
54
|
+
## Add to Codex
|
|
55
|
+
|
|
56
|
+
Start the server first:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
mcp-ssh-wrapper --host 127.0.0.1 --port 8000 --mount-path /mcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then register the running HTTP endpoint with Codex:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
codex mcp add mcp-ssh --url http://127.0.0.1:8000/mcp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Verify the registration:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
codex mcp list
|
|
72
|
+
codex mcp get mcp-ssh
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Codex will connect to the running HTTP endpoint. It does not launch the server process for you in this setup.
|
|
76
|
+
|
|
77
|
+
## Add to Claude
|
|
78
|
+
|
|
79
|
+
### Claude Code CLI
|
|
80
|
+
|
|
81
|
+
Start the server first, then register the URL with Claude Code:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
claude mcp add mcp-ssh --transport http http://127.0.0.1:8000/mcp
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Verify the registration:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
claude mcp list
|
|
91
|
+
claude mcp get mcp-ssh
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Claude Desktop
|
|
95
|
+
|
|
96
|
+
Add this server entry to your `claude_desktop_config.json`:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"mcp-ssh": {
|
|
102
|
+
"url": "http://127.0.0.1:8000/mcp"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Claude Desktop will connect to the running local HTTP server.
|
|
109
|
+
|
|
110
|
+
## Important limitation
|
|
111
|
+
|
|
112
|
+
`run_test.sh` does not launch the server. Start `mcp-ssh-wrapper ...` or `./run_server.sh ...` first, then run the test wrapper against the live endpoint.
|
|
113
|
+
|
|
114
|
+
## Local smoke test
|
|
115
|
+
|
|
116
|
+
To verify startup, MCP initialization, and tool discovery against a running server:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
./run_test.sh --skip-call
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
To target a non-default endpoint:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
./run_test.sh --url http://127.0.0.1:8000/mcp --skip-call
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
To also invoke the SSH tool:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
./run_test.sh --host my-host --command "uname -a"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Tool
|
|
135
|
+
|
|
136
|
+
### `execute_command`
|
|
137
|
+
|
|
138
|
+
Arguments:
|
|
139
|
+
|
|
140
|
+
- `host`: SSH host or `user@host`, resolved by the local SSH client
|
|
141
|
+
- `command`: Command string to execute remotely
|
|
142
|
+
- `timeout_seconds`: Optional timeout. `0` disables timeout handling.
|
|
143
|
+
|
|
144
|
+
Result:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"host": "prod-box",
|
|
149
|
+
"command": "uname -a",
|
|
150
|
+
"stdout": "Linux ...\n",
|
|
151
|
+
"stderr": "",
|
|
152
|
+
"exit_code": 0
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Notes
|
|
157
|
+
|
|
158
|
+
- SSH options should be configured in `~/.ssh/config` or the host environment.
|
|
159
|
+
- This server invokes `ssh -o BatchMode=yes`, so password prompts and interactive confirmations are disabled.
|
|
160
|
+
- Authentication, host key policy, and identity selection are still handled by the local `ssh` client and its existing configuration.
|
|
161
|
+
|
|
162
|
+
## Releasing
|
|
163
|
+
|
|
164
|
+
This project is configured to publish to PyPI using GitHub Trusted Publishing via [.github/workflows/publish-pypi.yml](mcp-ssh-wrapper/.github/workflows/publish-pypi.yml).
|
|
165
|
+
|
|
166
|
+
Release flow:
|
|
167
|
+
|
|
168
|
+
1. Update `project.version` in [pyproject.toml](mcp-ssh-wrapper/pyproject.toml).
|
|
169
|
+
2. Commit the version bump and push it to `main`.
|
|
170
|
+
3. Tag the `main` commit with a matching version tag in the form `vX.Y.Z`.
|
|
171
|
+
4. Push the tag to GitHub.
|
|
172
|
+
|
|
173
|
+
Example for version `0.1.0`:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
git checkout main
|
|
177
|
+
git pull
|
|
178
|
+
git tag v0.1.0
|
|
179
|
+
git push origin v0.1.0
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The GitHub Actions workflow will:
|
|
183
|
+
|
|
184
|
+
- verify the tag points to a commit on `main`
|
|
185
|
+
- verify the tag matches `project.version`
|
|
186
|
+
- build the package
|
|
187
|
+
- publish it to PyPI using the GitHub `pypi` environment
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcp-ssh-wrapper"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "An MCP server that executes remote commands through the host ssh binary."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "megascope" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mcp", "ssh", "server"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Internet",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"mcp>=1.0.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
mcp-ssh-wrapper = "mcp_ssh_wrapper.server:main"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/mcp_ssh_wrapper"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
repo_root="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
cd "$repo_root"
|
|
6
|
+
|
|
7
|
+
# If started outside an interactive shell, recover the macOS ssh-agent socket.
|
|
8
|
+
if [[ -z "${SSH_AUTH_SOCK:-}" ]] && command -v launchctl >/dev/null 2>&1; then
|
|
9
|
+
ssh_auth_sock="$(launchctl getenv SSH_AUTH_SOCK 2>/dev/null || true)"
|
|
10
|
+
if [[ -n "$ssh_auth_sock" ]]; then
|
|
11
|
+
export SSH_AUTH_SOCK="$ssh_auth_sock"
|
|
12
|
+
fi
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
exec uv run --python 3.10 --with mcp mcp-ssh-wrapper "$@"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from mcp import ClientSession
|
|
10
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_args() -> argparse.Namespace:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Smoke-test a running mcp-ssh-wrapper over streamable HTTP."
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--url",
|
|
19
|
+
default="http://127.0.0.1:8000/mcp",
|
|
20
|
+
help="MCP streamable HTTP endpoint. Defaults to http://127.0.0.1:8000/mcp.",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("--host", help="Remote SSH host to target for execute_command.")
|
|
23
|
+
parser.add_argument("--command", help="Remote command to execute.")
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--skip-call",
|
|
26
|
+
action="store_true",
|
|
27
|
+
help="Only initialize the server and list tools.",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--timeout-seconds",
|
|
31
|
+
type=int,
|
|
32
|
+
default=0,
|
|
33
|
+
help="Timeout forwarded to execute_command.",
|
|
34
|
+
)
|
|
35
|
+
return parser.parse_args()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def to_jsonable(value: Any) -> Any:
|
|
39
|
+
if hasattr(value, "model_dump"):
|
|
40
|
+
return value.model_dump(mode="json")
|
|
41
|
+
if isinstance(value, list):
|
|
42
|
+
return [to_jsonable(item) for item in value]
|
|
43
|
+
if isinstance(value, dict):
|
|
44
|
+
return {key: to_jsonable(item) for key, item in value.items()}
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def dump_message(title: str, message: Any) -> None:
|
|
49
|
+
print(f"== {title} ==")
|
|
50
|
+
print(json.dumps(to_jsonable(message), indent=2, sort_keys=True))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def run() -> int:
|
|
54
|
+
args = parse_args()
|
|
55
|
+
|
|
56
|
+
async with streamablehttp_client(args.url) as (read_stream, write_stream, _):
|
|
57
|
+
async with ClientSession(read_stream, write_stream) as session:
|
|
58
|
+
initialize = await session.initialize()
|
|
59
|
+
dump_message("initialize", initialize)
|
|
60
|
+
|
|
61
|
+
tools = await session.list_tools()
|
|
62
|
+
dump_message("tools/list", tools)
|
|
63
|
+
|
|
64
|
+
if not args.skip_call:
|
|
65
|
+
if not args.host or not args.command:
|
|
66
|
+
raise SystemExit("--host and --command are required unless --skip-call is used")
|
|
67
|
+
|
|
68
|
+
call = await session.call_tool(
|
|
69
|
+
"execute_command",
|
|
70
|
+
{
|
|
71
|
+
"host": args.host,
|
|
72
|
+
"command": args.command,
|
|
73
|
+
"timeout_seconds": args.timeout_seconds,
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
dump_message("tools/call", call)
|
|
77
|
+
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main() -> int:
|
|
82
|
+
return asyncio.run(run())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
raise SystemExit(main())
|