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.
@@ -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,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .coverage
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .venv
@@ -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,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "$0")" && pwd)"
5
+ cd "$repo_root"
6
+
7
+ exec uv run --python 3.10 --with mcp python scripts/test_http_client.py "$@"
@@ -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())
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from mcp_ssh_wrapper.server import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()