mcp-ssh-gateway 6.0.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_gateway-6.0.1/.github/ISSUE_TEMPLATE/bug_report.yml +47 -0
- mcp_ssh_gateway-6.0.1/.github/ISSUE_TEMPLATE/config.yml +5 -0
- mcp_ssh_gateway-6.0.1/.github/ISSUE_TEMPLATE/feature_request.yml +35 -0
- mcp_ssh_gateway-6.0.1/.github/PULL_REQUEST_TEMPLATE.md +24 -0
- mcp_ssh_gateway-6.0.1/.github/workflows/ci.yml +18 -0
- mcp_ssh_gateway-6.0.1/CHANGELOG.md +51 -0
- mcp_ssh_gateway-6.0.1/CONTRIBUTING.md +64 -0
- mcp_ssh_gateway-6.0.1/LICENSE +21 -0
- mcp_ssh_gateway-6.0.1/MANIFEST.in +14 -0
- mcp_ssh_gateway-6.0.1/PKG-INFO +148 -0
- mcp_ssh_gateway-6.0.1/QUICK_START.md +159 -0
- mcp_ssh_gateway-6.0.1/README.md +116 -0
- mcp_ssh_gateway-6.0.1/SECURITY.md +43 -0
- mcp_ssh_gateway-6.0.1/mcp-server.py +88 -0
- mcp_ssh_gateway-6.0.1/mcp.json.example +32 -0
- mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/PKG-INFO +148 -0
- mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/SOURCES.txt +41 -0
- mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/dependency_links.txt +1 -0
- mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/entry_points.txt +2 -0
- mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/requires.txt +5 -0
- mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/top_level.txt +1 -0
- mcp_ssh_gateway-6.0.1/pyproject.toml +70 -0
- mcp_ssh_gateway-6.0.1/requirements.txt +1 -0
- mcp_ssh_gateway-6.0.1/server.json +56 -0
- mcp_ssh_gateway-6.0.1/servers.json.example +36 -0
- mcp_ssh_gateway-6.0.1/setup.cfg +4 -0
- mcp_ssh_gateway-6.0.1/src/__init__.py +1 -0
- mcp_ssh_gateway-6.0.1/src/config.py +464 -0
- mcp_ssh_gateway-6.0.1/src/fs.py +1056 -0
- mcp_ssh_gateway-6.0.1/src/main.py +306 -0
- mcp_ssh_gateway-6.0.1/src/manager.py +881 -0
- mcp_ssh_gateway-6.0.1/src/py.typed +0 -0
- mcp_ssh_gateway-6.0.1/src/security.py +73 -0
- mcp_ssh_gateway-6.0.1/src/server.py +1113 -0
- mcp_ssh_gateway-6.0.1/src/session.py +2178 -0
- mcp_ssh_gateway-6.0.1/src/ssh_state.py +429 -0
- mcp_ssh_gateway-6.0.1/src/utils.py +984 -0
- mcp_ssh_gateway-6.0.1/tests/__init__.py +0 -0
- mcp_ssh_gateway-6.0.1/tests/test_fs.py +1488 -0
- mcp_ssh_gateway-6.0.1/tests/test_multiserver.py +1714 -0
- mcp_ssh_gateway-6.0.1/tests/test_output_contract.py +527 -0
- mcp_ssh_gateway-6.0.1/tests/test_server.py +1643 -0
- mcp_ssh_gateway-6.0.1/tests/test_ssh.py +3640 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something behaves differently from what the README promises.
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: what-happened
|
|
7
|
+
attributes:
|
|
8
|
+
label: What happened
|
|
9
|
+
description: Include the returned JSON, not a terminal screenshot.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: expected
|
|
14
|
+
attributes:
|
|
15
|
+
label: What you expected
|
|
16
|
+
validations:
|
|
17
|
+
required: true
|
|
18
|
+
- type: dropdown
|
|
19
|
+
id: host-type
|
|
20
|
+
attributes:
|
|
21
|
+
label: Host type
|
|
22
|
+
options:
|
|
23
|
+
- Linux / POSIX shell
|
|
24
|
+
- Vendor CLI (Keenetic NDM, Cisco-style, other)
|
|
25
|
+
- NAS / appliance
|
|
26
|
+
- VPS / cloud
|
|
27
|
+
validations:
|
|
28
|
+
required: true
|
|
29
|
+
- type: input
|
|
30
|
+
id: client
|
|
31
|
+
attributes:
|
|
32
|
+
label: MCP client
|
|
33
|
+
placeholder: Claude Desktop, Continue.dev, Cursor, ...
|
|
34
|
+
validations:
|
|
35
|
+
required: true
|
|
36
|
+
- type: input
|
|
37
|
+
id: version
|
|
38
|
+
attributes:
|
|
39
|
+
label: Server version
|
|
40
|
+
description: The `version` from the initialize response, or the git tag.
|
|
41
|
+
validations:
|
|
42
|
+
required: true
|
|
43
|
+
- type: textarea
|
|
44
|
+
id: call
|
|
45
|
+
attributes:
|
|
46
|
+
label: Exact tool call
|
|
47
|
+
description: The tools/call payload you sent.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: A capability the gateway is missing.
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: What problem does this solve?
|
|
9
|
+
description: Describe the workflow, not the implementation.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: proposal
|
|
14
|
+
attributes:
|
|
15
|
+
label: Proposed behaviour
|
|
16
|
+
validations:
|
|
17
|
+
required: true
|
|
18
|
+
- type: dropdown
|
|
19
|
+
id: host-type
|
|
20
|
+
attributes:
|
|
21
|
+
label: Which host type does this target?
|
|
22
|
+
options:
|
|
23
|
+
- Linux / POSIX shell
|
|
24
|
+
- Vendor CLI (Keenetic NDM, Cisco-style, other)
|
|
25
|
+
- NAS / appliance
|
|
26
|
+
- VPS / cloud
|
|
27
|
+
- Fleet-wide (all of the above)
|
|
28
|
+
validations:
|
|
29
|
+
required: true
|
|
30
|
+
- type: checkboxes
|
|
31
|
+
id: prompt-tokens
|
|
32
|
+
attributes:
|
|
33
|
+
label: Tool catalog
|
|
34
|
+
options:
|
|
35
|
+
- label: This needs a new tool (I accept the prompt-token cost for every user)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Pull request
|
|
2
|
+
description: A change to code, tests or documentation.
|
|
3
|
+
labels: ["change"]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: summary
|
|
7
|
+
attributes:
|
|
8
|
+
label: User-visible effect
|
|
9
|
+
description: What changes for someone running this server.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: contract
|
|
14
|
+
attributes:
|
|
15
|
+
label: Output contract
|
|
16
|
+
description: Confirm none of these regress: `has_more` counts unread lines; `line_limit` counts lines; `offset` never consumes; no internal markers or prompts reach the agent; non-zero exit stays `completed_nonzero`; every tool keeps its annotations.
|
|
17
|
+
- type: checkboxes
|
|
18
|
+
id: checks
|
|
19
|
+
attributes:
|
|
20
|
+
label: Checks
|
|
21
|
+
options:
|
|
22
|
+
- label: python -m unittest discover -s tests -t . passes
|
|
23
|
+
- label: Tests added or updated
|
|
24
|
+
- label: CHANGELOG.md updated under [Unreleased]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
timeout-minutes: 10
|
|
9
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
python-version: ["3.11", "3.13"]
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: ${{ matrix.python-version }}
|
|
17
|
+
- run: pip install -r requirements.txt
|
|
18
|
+
- run: python -m unittest discover -s tests -t . -v
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [6.0.1] — 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Rewrite the README around practical SSH fleet workflows, with a verified quickstart and clearer setup and security boundaries.
|
|
14
|
+
- Correct quickstart and security guidance; default the example host configuration to SSH host-key verification.
|
|
15
|
+
|
|
16
|
+
## [6.0.0] — 2026-09-28
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Unified scrollback: every tab keeps one 2M-character canvas addressed through a
|
|
20
|
+
single line-based cursor. `run` returns the first `line_limit` lines inline,
|
|
21
|
+
`has_more` reports the number of unread **lines**, and `read` delivers the next
|
|
22
|
+
window. No continuation token, no bookkeeping counters.
|
|
23
|
+
- Honest MCP framing: internal exit markers, prompt echoes and gateway machinery
|
|
24
|
+
never surface in the agent's window.
|
|
25
|
+
- Configuration is hot-reloaded on the health-loop pass (mtime + content hash), so
|
|
26
|
+
adding a host or tightening a policy no longer drops open sessions.
|
|
27
|
+
- Control requests (Ctrl+C, reads, session control) are served from a separate
|
|
28
|
+
pool, so an interrupt is never queued behind a long-running command.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- Vendor-CLI and pager support: `--More--` style pagers are auto-paginated, and
|
|
32
|
+
`shell: false` runs an appliance CLI (Keenetic NDM and similar) on a channel with
|
|
33
|
+
no shell wrapping, kept strictly separate from the POSIX shell tab.
|
|
34
|
+
- `file` tool: in-place remote search-and-replace edit with line-numbered
|
|
35
|
+
diagnostics, similarity hints, and an optional private `0600` `<path>.mcp.bak`.
|
|
36
|
+
- `--tool-profile lean`: 6 everyday tools, roughly half the catalog tokens.
|
|
37
|
+
- `--import-ssh-config`: register key-auth hosts from `~/.ssh/config`.
|
|
38
|
+
- Degraded mode: without paramiko the server still answers JSON-RPC with real
|
|
39
|
+
errors (-32700 / -32603) instead of hanging or returning an empty tool catalog.
|
|
40
|
+
- MCP client contract: negotiates protocol 2025-06-18 with fallback to older
|
|
41
|
+
revisions, sends `instructions` on initialize, and annotates every tool with
|
|
42
|
+
`readOnlyHint` / `destructiveHint` / `idempotentHint`.
|
|
43
|
+
|
|
44
|
+
### Security
|
|
45
|
+
- Per-host read-only guardrail and merged command blacklists, with local directory
|
|
46
|
+
containment for file transfers. Documented explicitly as *not* a security
|
|
47
|
+
boundary.
|
|
48
|
+
|
|
49
|
+
## [5.x] and earlier
|
|
50
|
+
|
|
51
|
+
See the commit history for the pre-6.0 line of development.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for looking at this. The project is small and self-contained, and PRs are welcome.
|
|
4
|
+
|
|
5
|
+
## Ground rules that are not negotiable
|
|
6
|
+
|
|
7
|
+
- **Do not break the output contract.** `has_more` is the number of unread *lines*.
|
|
8
|
+
`line_limit` counts lines, not characters. `offset` never consumes. Output reaching
|
|
9
|
+
the agent must contain no internal markers, no shell prompt, and no double-echoed
|
|
10
|
+
commands. The tests in `tests/test_server.py`, `tests/test_ssh.py`,
|
|
11
|
+
`tests/test_multiserver.py` and `tests/test_fs.py` enforce this.
|
|
12
|
+
- **Non-zero exit is a result, not an error.** `completed_nonzero` + `exit_status`
|
|
13
|
+
must never become a tool error. Only `failed`/`dead` are errors.
|
|
14
|
+
- **Every tool keeps its annotations.** `TOOL_ANNOTATIONS` in `src/server.py` must
|
|
15
|
+
cover the whole catalog, and the `lean` profile must keep annotations.
|
|
16
|
+
- **Initialize is a contract.** `protocolVersion` is negotiated against
|
|
17
|
+
`SUPPORTED_PROTOCOL_VERSIONS`, and `serverInfo` must use the public name
|
|
18
|
+
(`mcp-ssh`), never an internal codename.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/d00mus/MCP-SSH.git
|
|
24
|
+
cd MCP-SSH
|
|
25
|
+
pip install -r requirements.txt
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Python 3.11+ (CI also runs 3.13).
|
|
29
|
+
|
|
30
|
+
## Run the tests
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python -m unittest discover -s tests -t .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For one module:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m unittest tests.test_server -v
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The suite uses only `unittest` and `unittest.mock`, so no test dependencies are
|
|
43
|
+
needed. Most tests mock paramiko and never open a socket; if you add a test that
|
|
44
|
+
touches the network, mark it clearly and keep it out of the default path.
|
|
45
|
+
|
|
46
|
+
## Style
|
|
47
|
+
|
|
48
|
+
- Plain `unittest`, no pytest-only constructs.
|
|
49
|
+
- Comments explain *why*, especially around output framing and concurrency.
|
|
50
|
+
- Keep the tool catalog small. A new tool needs a good reason: it is prompt tokens
|
|
51
|
+
in every session for every user.
|
|
52
|
+
|
|
53
|
+
## Opening a change
|
|
54
|
+
|
|
55
|
+
1. Branch from `master`.
|
|
56
|
+
2. Add or update tests alongside the behaviour change.
|
|
57
|
+
3. Update `CHANGELOG.md` under `[Unreleased]`.
|
|
58
|
+
4. Open a pull request describing the user-visible effect, not just the diff.
|
|
59
|
+
|
|
60
|
+
## Reporting bugs
|
|
61
|
+
|
|
62
|
+
Use the issue templates. A useful report includes the MCP client, the host type
|
|
63
|
+
(POSIX shell vs vendor CLI), the exact tool call, and the returned JSON — not a
|
|
64
|
+
screenshot of the terminal.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 d00mus
|
|
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,14 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include QUICK_START.md
|
|
4
|
+
include CHANGELOG.md
|
|
5
|
+
include CONTRIBUTING.md
|
|
6
|
+
include SECURITY.md
|
|
7
|
+
include requirements.txt
|
|
8
|
+
include servers.json.example
|
|
9
|
+
include mcp.json.example
|
|
10
|
+
include server.json
|
|
11
|
+
include mcp-server.py
|
|
12
|
+
recursive-include src *.py
|
|
13
|
+
recursive-include tests *.py
|
|
14
|
+
recursive-include .github *.yml *.md
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-ssh-gateway
|
|
3
|
+
Version: 6.0.1
|
|
4
|
+
Summary: MCP server for a whole fleet of SSH hosts: persistent sessions with scrollback, explicit exit statuses, pager- and router-CLI-aware (Keenetic NDM), hot-reloaded config, honest output paging.
|
|
5
|
+
Author-email: Constantine Shklyarov <d00mus@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/d00mus/MCP-SSH
|
|
8
|
+
Project-URL: Repository, https://github.com/d00mus/MCP-SSH
|
|
9
|
+
Project-URL: Issues, https://github.com/d00mus/MCP-SSH/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/d00mus/MCP-SSH/blob/master/CHANGELOG.md
|
|
11
|
+
Keywords: mcp,model-context-protocol,ssh,ssh-gateway,ai-agents,ai-devops,multi-server,keenetic,router,paramiko
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: System :: Networking
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: paramiko>=3.0.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
30
|
+
Requires-Dist: twine>=5.1; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
<!-- mcp-name: io.github.d00mus/mcp-ssh-gateway -->
|
|
34
|
+
|
|
35
|
+
# MCP SSH Gateway
|
|
36
|
+
|
|
37
|
+
**Use your MCP client to work with several SSH hosts through one local connection.** Run diagnostics on a VPS, inspect a NAS or query a Keenetic router by name. Sessions preserve terminal state between calls; long output can be read a page at a time.
|
|
38
|
+
|
|
39
|
+
For people who already use SSH and want an assistant to help with routine diagnostics and administration. It is not an SSH daemon, a hosted proxy or a replacement for access controls on your servers.
|
|
40
|
+
|
|
41
|
+
[](https://github.com/d00mus/MCP-SSH/actions/workflows/ci.yml) [](https://www.python.org/downloads/) [](LICENSE)
|
|
42
|
+
|
|
43
|
+
## Try it with one host
|
|
44
|
+
|
|
45
|
+
You need Python 3.11+, an SSH account on a host you control and an MCP client that can launch a local stdio server. **PyPI publication of version 6.0.1 is pending.** Until it is published, start from this repository.
|
|
46
|
+
|
|
47
|
+
1. Clone the project and install its dependencies:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/d00mus/MCP-SSH.git
|
|
51
|
+
cd MCP-SSH
|
|
52
|
+
python -m pip install -r requirements.txt
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
2. Create `servers.json` in that directory (replace the address, user and key path with your own):
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"servers": {
|
|
60
|
+
"lab": {
|
|
61
|
+
"host": "192.168.1.10",
|
|
62
|
+
"user": "your-ssh-user",
|
|
63
|
+
"key_path": "~/.ssh/id_ed25519"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Host-key verification is enabled by default and uses the machine’s system host-key store. Ensure the host key is already trusted there, and verify its fingerprint independently before adding it. For password authentication, use `"password": "${LAB_SSH_PASSWORD}"` and provide `LAB_SSH_PASSWORD` to the MCP server process. Do not commit real credentials or your `servers.json`. See the [security policy](SECURITY.md).
|
|
70
|
+
|
|
71
|
+
3. Add this to a client that uses the `mcpServers` config format. **Replace both absolute paths**: clients do not necessarily start in the cloned directory.
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"ssh-gateway": {
|
|
77
|
+
"command": "python",
|
|
78
|
+
"args": [
|
|
79
|
+
"/absolute/path/to/MCP-SSH/mcp-server.py",
|
|
80
|
+
"--servers-config", "/absolute/path/to/MCP-SSH/servers.json"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
On Windows, point `command` to your Python executable if needed and use escaped backslashes in JSON paths (for example `C:\\work\\MCP-SSH\\mcp-server.py`). Restart the MCP client after updating its config. Running the script directly is not an interactive SSH terminal: it communicates with the client over stdio.
|
|
88
|
+
|
|
89
|
+
4. In the client, ask: **“List my SSH hosts, then run `uname -a` on lab.”** If the host is missing, check the config path and the client's MCP server logs. If SSH fails, check credentials and host-key verification.
|
|
90
|
+
|
|
91
|
+
Add more hosts under `servers` in the same file. [servers.json.example](servers.json.example) shows a multi-host configuration; check its host-key and credential choices before copying it.
|
|
92
|
+
|
|
93
|
+
## What using it looks like
|
|
94
|
+
|
|
95
|
+
A Linux host and a router can share one MCP connection. Your client makes calls like these (they are not terminal commands):
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
server_list() # find configured hosts
|
|
99
|
+
run(server="lab", command="df -h") # inspect disk space
|
|
100
|
+
run(server="keenetic", command="show interface", shell=false) # router CLI
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`run` returns a `session_id`; pass it to later calls if you need the same terminal state. Without it an idle session may be reused with unknown state; `new_session: true` forces a clean session. A command still running after the initial wait (5 seconds by default) reports `still_running: true`. Use `read(session_id="...")` for later output, or whenever `has_more` indicates unread lines. `signal(action="ctrl_c")` interrupts a stuck command. Non-zero exits report `completed_nonzero` and `exit_status`, not silent success.
|
|
104
|
+
|
|
105
|
+
The `file` tool can inspect and edit remote files through SFTP (with shell fallback). Review edits and give an assistant only the SSH permissions it needs.
|
|
106
|
+
|
|
107
|
+
## When to use it
|
|
108
|
+
|
|
109
|
+
- **Multiple hosts:** one MCP server configuration routes calls to named targets. For just one host, this matters less.
|
|
110
|
+
- **Multi-step troubleshooting:** persistent sessions keep shell state, while line-based output windows avoid dumping an entire log into the conversation at once.
|
|
111
|
+
- **A Keenetic alongside Linux hosts:** `shell: false` sends device CLI commands without a POSIX shell; common pagers such as `--More--` are handled. Keenetic NDM is a supported use case, but other vendor CLIs are not guaranteed. Keep NDM CLI and Linux shell operations in separate sessions.
|
|
112
|
+
|
|
113
|
+
**Security boundary:** `read_only` and command blacklists are best-effort guardrails against mistakes, not a sandbox. Shell expansion and interpreters can bypass checks on command text. Use restricted SSH users and server-side permissions for sensitive hosts. Host-key verification is on by default; avoid turning it off casually.
|
|
114
|
+
|
|
115
|
+
The SSH connection originates from the machine running the gateway. This project works with MCP clients that can start a stdio server; it does not add SSH access to a chat app without MCP integration.
|
|
116
|
+
|
|
117
|
+
## Other ways to run it
|
|
118
|
+
|
|
119
|
+
**Docker (build from this clone):**
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
docker build -t mcp-ssh-server .
|
|
123
|
+
docker run -i --rm \
|
|
124
|
+
-v /absolute/path/to/servers.json:/app/servers.json:ro \
|
|
125
|
+
-v /absolute/path/to/your/.ssh:/root/.ssh:ro \
|
|
126
|
+
mcp-ssh-server --servers-config /app/servers.json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Use absolute mount paths and pass required environment variables with `-e NAME`. This example exposes SSH keys to the container; mount only what it needs. For an MCP client using Docker, set `command` to `docker` and put the same run arguments in `args`.
|
|
130
|
+
|
|
131
|
+
**PyPI / MCP Registry:** PyPI publication of version 6.0.1 and the MCP Registry listing are pending. Until published, `pip install mcp-ssh-gateway` and `uvx --from mcp-ssh-gateway ...` will not work. See the [release process](docs/PUBLISHING.md). No publication badge is shown until there is a listing.
|
|
132
|
+
|
|
133
|
+
## Configuration and tools
|
|
134
|
+
|
|
135
|
+
- Each target has an alias, `host`, `user`, optional `port` (default 22) and a `key_path` or `password`. `verify_host` defaults to `true`. `password` and `key_passphrase` support environment references (`${NAME}`); missing references fail at startup.
|
|
136
|
+
- The default full profile exposes `server_list`, `server_add`, `run`, `read`, `signal`, `file`, `session_list`, `session_update`, `session_close` and `last_command_details`. `server_add` accepts an `alias` and only appends new targets. `--tool-profile lean` exposes six everyday tools for a smaller catalog.
|
|
137
|
+
- Changes to `servers.json` are checked periodically (every 30 seconds); `server_list(reload=true)` checks immediately. Unchanged hosts keep their sessions; removing a host or changing its address, login or host-key settings closes that host’s active sessions.
|
|
138
|
+
- `--log-output meta` (the default) records lifecycle information and command text. `full` also records raw output; `off` disables logging. Consider what secrets might appear in commands and output.
|
|
139
|
+
|
|
140
|
+
For contributions or vulnerabilities, see [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python -m unittest discover -s tests -t .
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
See the [changelog](CHANGELOG.md). MIT-licensed; see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Quick Start: Multi-Server SSH MCP Gateway
|
|
2
|
+
|
|
3
|
+
Connect an MCP client to your SSH hosts through one locally running gateway. Start with the simpler [README setup](README.md#try-it-with-one-host); the examples below cover more options.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Prepare Configuration (`servers.json`)
|
|
8
|
+
|
|
9
|
+
Create `servers.json` at the absolute path you will pass with `--servers-config` (or copy [servers.json.example](servers.json.example)):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"servers": {
|
|
14
|
+
"keenetic": {
|
|
15
|
+
"host": "192.168.1.1",
|
|
16
|
+
"port": 22,
|
|
17
|
+
"user": "admin",
|
|
18
|
+
"password": "${KEENETIC_PASSWORD}",
|
|
19
|
+
"description": "Keenetic Ultra router",
|
|
20
|
+
"verify_host": true,
|
|
21
|
+
"extra_path": "/opt/bin:/opt/sbin"
|
|
22
|
+
},
|
|
23
|
+
"nas": {
|
|
24
|
+
"host": "192.168.1.10",
|
|
25
|
+
"port": 22,
|
|
26
|
+
"user": "storage",
|
|
27
|
+
"key_path": "~/.ssh/id_rsa",
|
|
28
|
+
"description": "TrueNAS storage",
|
|
29
|
+
"max_sessions": 10
|
|
30
|
+
},
|
|
31
|
+
"vps-prod": {
|
|
32
|
+
"host": "203.0.113.5",
|
|
33
|
+
"port": 2222,
|
|
34
|
+
"user": "ubuntu",
|
|
35
|
+
"key_path": "~/.ssh/vps_key",
|
|
36
|
+
"read_only": true,
|
|
37
|
+
"command_blacklist": ["reboot", "poweroff", "rm -rf"],
|
|
38
|
+
"description": "Production web server (read-only)"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> **Before connecting:** verify and add the SSH host key to the system host-key store on the machine running the gateway. `verify_host: true` rejects unknown host keys. `${KEENETIC_PASSWORD}` must be set in the MCP process environment. `read_only` and command blacklists are mistake guards, not a security boundary; use restricted SSH accounts. See [SECURITY.md](SECURITY.md).
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 2. Installation & Running
|
|
49
|
+
|
|
50
|
+
### Path A: Python (Fastest)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install -r requirements.txt
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Path B: Docker
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
docker build -t mcp-ssh-server .
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 3. Configure your AI Agent
|
|
65
|
+
|
|
66
|
+
### MCP Client Config (`mcp.json`)
|
|
67
|
+
|
|
68
|
+
**With Python (Windows example):**
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"ssh-gateway": {
|
|
73
|
+
"command": "python",
|
|
74
|
+
"args": [
|
|
75
|
+
"C:\\tools\\ssh-gateway\\mcp-server.py",
|
|
76
|
+
"--servers-config", "C:\\tools\\ssh-gateway\\servers.json",
|
|
77
|
+
"--project-root", "C:\\work"
|
|
78
|
+
],
|
|
79
|
+
"env": {
|
|
80
|
+
"KEENETIC_PASSWORD": "your_secure_password"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**With Docker:**
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"ssh-gateway": {
|
|
93
|
+
"command": "docker",
|
|
94
|
+
"args": [
|
|
95
|
+
"run", "-i", "--rm",
|
|
96
|
+
"-v", "C:/tools/ssh-gateway/servers.json:/app/servers.json:ro",
|
|
97
|
+
"-v", "C:/Users/username/.ssh:/root/.ssh:ro",
|
|
98
|
+
"-v", "C:/tools/ssh-gateway/.ssh-cache:/app/.ssh-cache",
|
|
99
|
+
"mcp-ssh-server",
|
|
100
|
+
"--servers-config", "/app/servers.json"
|
|
101
|
+
],
|
|
102
|
+
"env": {
|
|
103
|
+
"KEENETIC_PASSWORD": "your_secure_password"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"ssh-gateway": {
|
|
116
|
+
"command": "python",
|
|
117
|
+
"args": [
|
|
118
|
+
"/opt/ssh-gateway/mcp-server.py",
|
|
119
|
+
"--servers-config", "/opt/ssh-gateway/servers.json"
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### 4. Key Agent Workflows & Best Practices
|
|
129
|
+
|
|
130
|
+
1. **Discover Servers:**
|
|
131
|
+
The AI calls `server_list` to see all configured hosts, status, and active sessions. `server_list` always provides inline `active_sessions: [{"session_id": "...", "status": "...", "mode": "..."}]` in both profiles, eliminating the need for `session_list`.
|
|
132
|
+
2. **Execute Commands (`run`):**
|
|
133
|
+
- Explicit target: `run(server="keenetic", command="show interface", shell=false)`
|
|
134
|
+
- Composite session ID: `run(session_id="nas/1", command="zpool status")`
|
|
135
|
+
- **Direct Output:** Commands that complete within 5.0s return their first output window directly. Call `read` only if `has_more` reports unread lines, or if a command is still running; a completed command with `has_more: 0` needs no extra read.
|
|
136
|
+
- **Long-Running Commands:** Commands taking longer than 5.0s return `still_running: true` without failing. Read their remaining output via `read(session_id="...")`. `status` may also be `completed_nonzero`, `interrupted` (e.g. when `hard_timeout` fired; partial output kept) or `stalled` (quiet idle with no end-of-command marker; adds `unconfirmed_completion: true`).
|
|
137
|
+
- **Sequential vs Concurrent:** Pass `session_id` to run sequential commands in the same session. Omit `session_id` to reuse an idle session (or pass `new_session: true` for a clean one).
|
|
138
|
+
- **Async Execution:** Pass `wait_timeout: 0` for immediate return while the command continues in its session.
|
|
139
|
+
- **Standard Shell Pipelines:** Use standard `| grep`, `| awk`, `| head` inside the command string for filtering.
|
|
140
|
+
- **Single non-interactive commands:** Pass `use_pty: false` for pure single exec commands with closed `stdin` (bypasses terminal echoes and PTY line limits; note: interactive heredocs are not supported when `use_pty: false`).
|
|
141
|
+
3. **Session Management (1 Session = 1 Terminal):**
|
|
142
|
+
- An SSH session is a single terminal process (PTY). Never send concurrent commands to the same session.
|
|
143
|
+
- Reuse `session_id` for sequential steps; pass `new_session: true` to execute commands in parallel in a clean session (omitting `session_id` reuses an idle one).
|
|
144
|
+
- Close temporary diagnostic sessions with `session_close` when done to free the session limit.
|
|
145
|
+
- For Keenetic: keep NDM CLI (`shell: false`) and Linux shell (`shell: true`) in separate sessions.
|
|
146
|
+
4. **Buffered Output, Scrolling & Canvas Windowing (`read`):**
|
|
147
|
+
- **One line-based stream** (the tab canvas): `line_limit` (default 200 lines, max 5000, `0` = no line cap), `tail` (last N lines; moves the unread position to the end and reports skipped unread lines as `dropped_data`), `offset` (**line** position: negative=peek back from the cursor, `0`=inspect from the very first line, positive=inspect from line N; any `offset` is a non-consuming peek - the unread cursor does NOT move and progress is preserved). `has_more` is the **number of unread LINES still left** (`0` = caught up) - repeat the same read to continue.
|
|
148
|
+
- Paging is server-side: there is no continuation token (the old `cursor` argument was removed and is rejected) - a plain `read(session_id)` continues with the unread output.
|
|
149
|
+
- Per-tab history holds up to 2,000,000 characters; output is mirrored into the canvas and completed run buffers are evicted under a global budget, so `offset: 0` still re-reads whatever the canvas holds (`dropped_data` reports unread text dropped before it could be delivered).
|
|
150
|
+
- The response also carries `status` (`running`, `completed`, `completed_nonzero`, `interrupted`, `stalled`) and `still_running`; a window cut by `line_limit` is reported through `has_more` plus a `hint` (raise line_limit or read again). `wait_timeout` waits for completion **or** new output and blocks only while the stream is silent.
|
|
151
|
+
5. **Interrupt Stuck Commands (`signal`):**
|
|
152
|
+
- Call `signal(action="ctrl_c")` to immediately terminate a hanging command and free the session.
|
|
153
|
+
6. **Remote Files (`file`):**
|
|
154
|
+
- Inspect or download files with `action: "read"`. Supports line-based pagination (`offset_line: 1`, `limit_lines: 200`, returning `next_offset_line`).
|
|
155
|
+
- Modify remote files with `action: "edit"` and search-and-replace (`edits: [{"old_text": "...", "new_text": "..."}]`); `create_backup: true` requests a `.mcp.bak` backup. Do not assume every remote filesystem or shell fallback guarantees atomic replacement or private permissions.
|
|
156
|
+
7. **Register New Servers On The Fly:**
|
|
157
|
+
- In full profile, the AI can call `server_add(alias="staging-app", host="10.0.0.5", user="deploy")` without restarting the server.
|
|
158
|
+
8. **Live Hot-Reload:**
|
|
159
|
+
- Edit credentials or servers in `servers.json` — the gateway detects changes automatically on the next health-loop pass (every 30 seconds), or immediately via `server_list(reload: true)`, with zero downtime for unaffected sessions.
|