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.
Files changed (43) hide show
  1. mcp_ssh_gateway-6.0.1/.github/ISSUE_TEMPLATE/bug_report.yml +47 -0
  2. mcp_ssh_gateway-6.0.1/.github/ISSUE_TEMPLATE/config.yml +5 -0
  3. mcp_ssh_gateway-6.0.1/.github/ISSUE_TEMPLATE/feature_request.yml +35 -0
  4. mcp_ssh_gateway-6.0.1/.github/PULL_REQUEST_TEMPLATE.md +24 -0
  5. mcp_ssh_gateway-6.0.1/.github/workflows/ci.yml +18 -0
  6. mcp_ssh_gateway-6.0.1/CHANGELOG.md +51 -0
  7. mcp_ssh_gateway-6.0.1/CONTRIBUTING.md +64 -0
  8. mcp_ssh_gateway-6.0.1/LICENSE +21 -0
  9. mcp_ssh_gateway-6.0.1/MANIFEST.in +14 -0
  10. mcp_ssh_gateway-6.0.1/PKG-INFO +148 -0
  11. mcp_ssh_gateway-6.0.1/QUICK_START.md +159 -0
  12. mcp_ssh_gateway-6.0.1/README.md +116 -0
  13. mcp_ssh_gateway-6.0.1/SECURITY.md +43 -0
  14. mcp_ssh_gateway-6.0.1/mcp-server.py +88 -0
  15. mcp_ssh_gateway-6.0.1/mcp.json.example +32 -0
  16. mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/PKG-INFO +148 -0
  17. mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/SOURCES.txt +41 -0
  18. mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/dependency_links.txt +1 -0
  19. mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/entry_points.txt +2 -0
  20. mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/requires.txt +5 -0
  21. mcp_ssh_gateway-6.0.1/mcp_ssh_gateway.egg-info/top_level.txt +1 -0
  22. mcp_ssh_gateway-6.0.1/pyproject.toml +70 -0
  23. mcp_ssh_gateway-6.0.1/requirements.txt +1 -0
  24. mcp_ssh_gateway-6.0.1/server.json +56 -0
  25. mcp_ssh_gateway-6.0.1/servers.json.example +36 -0
  26. mcp_ssh_gateway-6.0.1/setup.cfg +4 -0
  27. mcp_ssh_gateway-6.0.1/src/__init__.py +1 -0
  28. mcp_ssh_gateway-6.0.1/src/config.py +464 -0
  29. mcp_ssh_gateway-6.0.1/src/fs.py +1056 -0
  30. mcp_ssh_gateway-6.0.1/src/main.py +306 -0
  31. mcp_ssh_gateway-6.0.1/src/manager.py +881 -0
  32. mcp_ssh_gateway-6.0.1/src/py.typed +0 -0
  33. mcp_ssh_gateway-6.0.1/src/security.py +73 -0
  34. mcp_ssh_gateway-6.0.1/src/server.py +1113 -0
  35. mcp_ssh_gateway-6.0.1/src/session.py +2178 -0
  36. mcp_ssh_gateway-6.0.1/src/ssh_state.py +429 -0
  37. mcp_ssh_gateway-6.0.1/src/utils.py +984 -0
  38. mcp_ssh_gateway-6.0.1/tests/__init__.py +0 -0
  39. mcp_ssh_gateway-6.0.1/tests/test_fs.py +1488 -0
  40. mcp_ssh_gateway-6.0.1/tests/test_multiserver.py +1714 -0
  41. mcp_ssh_gateway-6.0.1/tests/test_output_contract.py +527 -0
  42. mcp_ssh_gateway-6.0.1/tests/test_server.py +1643 -0
  43. 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,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Security vulnerability
4
+ url: https://github.com/d00mus/MCP-SSH/security/advisories/new
5
+ about: Report privately — do not open a public issue.
@@ -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
+ [![CI](https://github.com/d00mus/MCP-SSH/actions/workflows/ci.yml/badge.svg)](https://github.com/d00mus/MCP-SSH/actions/workflows/ci.yml) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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.