@neoline/hostbridge 0.2.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hostbridge contributors
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.
package/README.md ADDED
@@ -0,0 +1,350 @@
1
+ # hostbridge
2
+
3
+ A local MCP server for AI agents to operate allowlisted interactive shells, usually SSH sessions. It keeps PTYs alive in memory, so an agent can open a session once, run quick commands, start long-running background jobs, and poll logs until completion.
4
+
5
+ This repo is portable: it ships with no personal server paths or built-in hosts. Each user declares their own connection targets in `~/.hostbridge/hosts.json` or a path set by `HOSTBRIDGE_HOSTS_FILE`.
6
+
7
+ ## Tools
8
+
9
+ - `hosts_list` — reload and show configured connection targets and local script availability when applicable.
10
+ - `hosts_reload` — explicitly reload configured connection targets without restarting the MCP server.
11
+ - `session_open(host_id, connect_timeout=30)` — open a persistent interactive shell for one configured host.
12
+ - `sessions_list()` — list live sessions in this MCP process.
13
+ - `sessions_close_all()` — close every live session in one call; useful for shutdown and cleanup.
14
+ - `command_run(session_id, command, timeout=60)` — run a foreground command and wait for completion.
15
+ - `task_start(session_id, command)` — start a remote background job under `$HOME/.hostbridge/jobs/<task_id>`.
16
+ - `task_status(session_id, task_id, tail_lines=80)` — poll job state, exit code, and log tail.
17
+ - `task_stop(session_id, task_id)` — terminate a running background job and reap its process.
18
+ - `session_write(session_id, text)` — send a raw line for interactive edge cases.
19
+ - `session_read(session_id, timeout=1.0, max_chars=20000)` — read available raw output after interactive writes.
20
+ - `session_close(session_id)` — close a persistent session.
21
+
22
+ Every tool returns a structured payload with a stable `error_code` on failure (`HOST_NOT_FOUND`, `SESSION_NOT_FOUND`, `POLICY_DENIED`, `TASK_NOT_FOUND`, `TIMEOUT`, `INTERNAL`).
23
+
24
+ ## Install, connect to agents, and configure hosts
25
+
26
+ The lifecycle mirrors tools like CodeGraph: install the CLI, register the MCP with local agent clients, then configure per-machine host entries. Installing the CLI alone does not connect it to any agent; `hostbridge install` performs that wiring. It does not configure any servers; per-server setup is the separate `hostbridge setup` step.
27
+
28
+ The shared distribution path is npm. Users can run the MCP server or setup wizard with `npx`:
29
+
30
+ ```bash
31
+ npx -y @neoline/hostbridge install
32
+ npx -y @neoline/hostbridge setup
33
+ npx -y @neoline/hostbridge list
34
+ ```
35
+
36
+ For a persistent global install:
37
+
38
+ ```bash
39
+ npm install -g @neoline/hostbridge
40
+ hostbridge install
41
+ hostbridge setup
42
+ ```
43
+
44
+ `hostbridge install` detects supported local agents and writes only the hostbridge MCP entry into each detected config, preserving existing MCP servers. Supported targets are:
45
+
46
+ - Codex CLI: `~/.codex/config.toml`
47
+ - Claude Code: `~/.claude.json`
48
+ - Cursor: `~/.cursor/mcp.json`
49
+ - Gemini CLI: `~/.gemini/settings.json`
50
+ - opencode: `~/.config/opencode/opencode.json`
51
+ - Hermes Agent: `~/.hermes/mcp.json`
52
+ - Antigravity IDE: `~/.antigravity/mcp.json`
53
+ - Kiro: `~/.kiro/settings/mcp.json`
54
+
55
+ Use `--target <name>` for one client only, for example `hostbridge install --target codex`. From a source checkout, the installer registers `python3 <repo>/run_server.py`. From an installed package, it registers the `hostbridge` executable. Restart open agent clients or start new sessions once after installation so the new MCP server is loaded.
56
+
57
+ This npm package is a Node launcher around the Python MCP core, so the machine still needs Python 3.11+ and the package's Python dependencies available to that interpreter. To point at a non-default interpreter:
58
+
59
+ ```bash
60
+ HOSTBRIDGE_PYTHON=/path/to/python3 npx -y @neoline/hostbridge setup
61
+ ```
62
+
63
+ For local development from this repository:
64
+
65
+ ```bash
66
+ git clone <repo-url> hostbridge
67
+ cd hostbridge
68
+ python3 -m pip install -e .
69
+ python3 -m server_control_mcp.server install
70
+ npm run test:node
71
+ ```
72
+
73
+ ## Guided setup wizard
74
+
75
+ Run the local setup wizard instead of hand-writing `hosts.json`:
76
+
77
+ ```bash
78
+ hostbridge setup
79
+ ```
80
+
81
+ You can also pass the host id up front to reduce prompts:
82
+
83
+ ```bash
84
+ hostbridge setup gpu_1
85
+ ```
86
+
87
+ The wizard now records the real login path instead of asking a fixed questionnaire:
88
+
89
+ 1. Collect only a stable host id, display label, and the command you already run locally, such as `ssh bastion`, `ssh -J bastion alice@gpu.internal`, `bash ~/.ssh/connect_gpu.sh`, or `docker exec -it training-box bash`.
90
+ 2. Start that command in a terminal-style PTY proxy: remote output is printed directly, and user input is forwarded to the setup session.
91
+ 3. Record the prompt visible when each line is typed. Secret-looking prompts (`password`, `passphrase`, `token`, `OTP`, `密码`, `验证码`) do not echo locally and are encrypted before saving.
92
+ 4. Convert the recorded prompt/input pairs into ordered `login_steps`; type `/ready` once the final remote shell prompt is available.
93
+ 5. Send a readiness probe through the same session, then replay the recorded `login_steps` in a fresh connection before saving `~/.hostbridge/hosts.json`.
94
+
95
+ Useful helper commands:
96
+
97
+ ```bash
98
+ hostbridge install
99
+ hostbridge list
100
+ hostbridge check gpu_1
101
+ hostbridge remove gpu_1
102
+ hostbridge setup --config ./hosts.json
103
+ hostbridge reload
104
+ ```
105
+
106
+ ### Agent-friendly setup
107
+
108
+ Agents should prefer the CLI setup flow over searching shell history or hand-editing JSON. A common JumpServer pattern is to clone a known bastion entry and change only the menu-selected host:
109
+
110
+ ```bash
111
+ hostbridge setup gpu_lab_1 \
112
+ --label "Lab GPU server" \
113
+ --from-host bastion \
114
+ --menu-selection "gpu-lab-01" \
115
+ --final-prompt '.*[$#] '
116
+ ```
117
+
118
+ This reuses the source host's connection command and encrypted secrets, replaces the menu selection, verifies the resulting runtime path, then saves the new host. After setup, running MCP servers that include runtime reload support will pick up the new entry on the next `hosts_list` or `session_open`; no client restart is required. If an already-running MCP process predates this feature, restart it once.
119
+
120
+ Recommended agent flow:
121
+
122
+ 1. Call `hosts_list`.
123
+ 2. If the target host exists, call `session_open`.
124
+ 3. If it does not exist and the user identifies a similar bastion-backed host, run `hostbridge setup <new_id> --from-host <source_id> --menu-selection <ip-or-name> --final-prompt <regex>`.
125
+ 4. Call `hosts_list` again, then `session_open(<new_id>)`.
126
+ 5. Do not inspect `~/.ssh`, old expect scripts, or shell snapshots unless the user explicitly asks or the setup command lacks required information.
127
+
128
+ ### Encrypted login automation
129
+
130
+ Some environments require a password prompt, a bastion menu, or a JumpServer host selector before the remote shell is available. The setup wizard can save these as ordered `login_steps`. Secrets are encrypted with a local Fernet key at `~/.hostbridge/key` or `HOSTBRIDGE_KEY_FILE`; the decrypted value is used only in memory while opening a session.
131
+
132
+ Equivalent non-interactive commands are available:
133
+
134
+ ```bash
135
+ hostbridge secret set bastion password
136
+ hostbridge step add bastion --expect '(?i)password:' --send-secret password
137
+ hostbridge step add bastion --expect 'Opt>' --send 'gpu-lab-01'
138
+ hostbridge step add bastion --expect '.*[$#] ' --send ''
139
+ ```
140
+
141
+ Security model:
142
+
143
+ - `hosts.json` never stores plaintext passwords.
144
+ - `hostbridge list` and `hosts_list` do not expose secret material.
145
+ - `~/.hostbridge/hosts.json` and `~/.hostbridge/key` should be readable only by the local user running the MCP server.
146
+ - Local encryption protects config-at-rest, not a fully compromised same-user account. Prefer SSH keys or system keychains when available.
147
+
148
+ ## Configure hosts
149
+
150
+ Create `~/.hostbridge/hosts.json`:
151
+
152
+ ```bash
153
+ mkdir -p ~/.hostbridge
154
+ cat > ~/.hostbridge/hosts.json <<'JSON'
155
+ {
156
+ "hosts": [
157
+ {
158
+ "id": "gpu_direct",
159
+ "label": "GPU direct SSH",
160
+ "ssh": {
161
+ "host": "alice@gpu.example.com"
162
+ }
163
+ }
164
+ ]
165
+ }
166
+ JSON
167
+ ```
168
+
169
+ Running MCP servers reload this file during `hosts_list` and `session_open`, so a client restart is not normally required after editing it.
170
+
171
+ ### Direct SSH, no jump host
172
+
173
+ ```json
174
+ {
175
+ "hosts": [
176
+ {
177
+ "id": "gpu",
178
+ "label": "GPU server",
179
+ "ssh": {
180
+ "host": "alice@gpu.example.com"
181
+ }
182
+ }
183
+ ]
184
+ }
185
+ ```
186
+
187
+ ### SSH with port, key, and jump host
188
+
189
+ ```json
190
+ {
191
+ "hosts": [
192
+ {
193
+ "id": "gpu_jump",
194
+ "label": "GPU via bastion",
195
+ "ssh": {
196
+ "host": "alice@gpu.internal",
197
+ "port": 2222,
198
+ "identity_file": "~/.ssh/gpu_key",
199
+ "proxy_jump": "bastion.example.com",
200
+ "extra_args": ["-o", "ServerAliveInterval=30"]
201
+ }
202
+ }
203
+ ]
204
+ }
205
+ ```
206
+
207
+ ### Existing shell script wrapper
208
+
209
+ Use this if you already have a `.sh` or expect wrapper that opens an interactive remote shell.
210
+
211
+ ```json
212
+ {
213
+ "hosts": [
214
+ {
215
+ "id": "legacy_gpu",
216
+ "label": "Legacy GPU wrapper",
217
+ "script": "~/.ssh/connect_gpu.sh"
218
+ }
219
+ ]
220
+ }
221
+ ```
222
+
223
+ The wrapper does not need executable permission; the MCP runs it with `/bin/sh`.
224
+
225
+ ### Advanced command mode
226
+
227
+ Use this for non-SSH interactive shells, containers, or custom connection tools. The MCP calls the executable directly without `shell=True`.
228
+
229
+ ```json
230
+ {
231
+ "hosts": [
232
+ {
233
+ "id": "container",
234
+ "label": "Local container shell",
235
+ "command": "docker",
236
+ "args": ["exec", "-it", "training-box", "bash"]
237
+ }
238
+ ]
239
+ }
240
+ ```
241
+
242
+ ## Host config rules
243
+
244
+ - `id` may contain only letters, numbers, `_`, `-`, and `.`.
245
+ - Each host must define exactly one practical connection style: `ssh`, `script`, or `command`.
246
+ - `ssh.host` can be any target accepted by `ssh`, such as `user@example.com` or an alias from `~/.ssh/config`.
247
+ - `ssh.identity_file` and `script` expand `~` for the local user running the MCP server.
248
+ - For a different config path, set `HOSTBRIDGE_HOSTS_FILE=/path/to/hosts.json` in the MCP server environment.
249
+
250
+ ## MCP client config
251
+
252
+ Prefer the installer:
253
+
254
+ ```bash
255
+ hostbridge install
256
+ ```
257
+
258
+ Single-client install is also available:
259
+
260
+ ```bash
261
+ hostbridge install --target codex
262
+ hostbridge install --target cursor
263
+ hostbridge install --target opencode
264
+ ```
265
+
266
+ Codex config written by the installer, for example in `~/.codex/config.toml`:
267
+
268
+ ```toml
269
+ [mcp_servers.hostbridge]
270
+ command = "npx"
271
+ args = ["-y", "hostbridge"]
272
+ ```
273
+
274
+ Claude Desktop style config:
275
+
276
+ ```json
277
+ {
278
+ "mcpServers": {
279
+ "hostbridge": {
280
+ "command": "npx",
281
+ "args": ["-y", "hostbridge"]
282
+ }
283
+ }
284
+ }
285
+ ```
286
+
287
+ To use a project-specific host file:
288
+
289
+ ```toml
290
+ [mcp_servers.hostbridge.env]
291
+ HOSTBRIDGE_HOSTS_FILE = "/absolute/path/to/hosts.json"
292
+ ```
293
+
294
+ See `examples/` for copyable config templates.
295
+
296
+ ## Policy, audit, and idle reaping
297
+
298
+ hostbridge ships a lightweight policy layer that runs in the same process as the MCP server. It is on by default and can be tuned or disabled via environment variables or a JSON config file.
299
+
300
+ Environment variables:
301
+
302
+ | Variable | Effect |
303
+ | --- | --- |
304
+ | `HOSTBRIDGE_DISABLE_POLICY=1` | Disable policy checks and audit logging entirely. |
305
+ | `HOSTBRIDGE_POLICY_FILE=/path/to/policy.json` | Override policy config path (default `~/.hostbridge/policy.json`). |
306
+ | `HOSTBRIDGE_DENYLIST=pat1:pat2` | Append extra deny regex patterns (colon-separated). |
307
+ | `HOSTBRIDGE_IDLE_TIMEOUT=900` | Idle timeout in seconds before an inactive session is reaped (default 1800). |
308
+ | `HOSTBRIDGE_AUDIT_LOG=/path/to/audit.jsonl` | Override audit log path (default `~/.hostbridge/audit.jsonl`). |
309
+
310
+ Default denylist blocks obviously destructive commands (`rm -rf /`, `mkfs`, `dd of=/dev/sd*`, fork bombs, `shutdown`, `reboot`). A denied command returns `error_code=POLICY_DENIED` with the matched pattern, and is recorded in the audit log alongside session/task lifecycle events.
311
+
312
+ Policy config file (optional):
313
+
314
+ ```json
315
+ {
316
+ "idle_timeout_seconds": 1800,
317
+ "use_default_denylist": true,
318
+ "command_denylist": ["\\bmytool\\b"],
319
+ "audit_log_path": "~/.hostbridge/audit.jsonl"
320
+ }
321
+ ```
322
+
323
+ See `SECURITY.md` and `docs/THREAT_MODEL.md` for the trust model and audited events.
324
+
325
+ ## Typical agent workflow
326
+
327
+ 1. Call `hosts_list` and choose a configured host id.
328
+ 2. If the host is missing, ask for only the missing fields and use `hostbridge setup <host_id>` rather than manually editing config.
329
+ 3. Call `session_open` once for the chosen host.
330
+ 4. Use `command_run` for quick commands such as `nvidia-smi`, `pwd`, `df -h`, or `tmux ls`.
331
+ 5. Use `task_start` for long training jobs, downloads, builds, or scripts.
332
+ 6. Poll `task_status` until `state` is `finished`; inspect `exit_code` and `log_tail`.
333
+ 7. Call `task_stop` early if a job should be aborted.
334
+ 8. Call `session_close` when finished, or `sessions_close_all` to tear down every session.
335
+
336
+ ## Security notes
337
+
338
+ - Host access is allowlisted by config; MCP callers cannot provide arbitrary local connection commands at runtime.
339
+ - The server never reads or logs passwords from underlying SSH/script prompts.
340
+ - Commands still run with your remote shell privileges. Use only with trusted local MCP clients.
341
+ - Background task logs and exit codes are stored on the remote host in `$HOME/.hostbridge/jobs/`.
342
+ - The policy layer blocks a built-in destructive denylist and writes structured audit events. It is defense-in-depth, not a sandbox.
343
+
344
+ ## Contributing
345
+
346
+ See `CONTRIBUTING.md`. Run tests with `python -m pytest -q`. The full suite should pass before pushing.
347
+
348
+ ## License
349
+
350
+ MIT. See `LICENSE`.
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { dirname, join, delimiter } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const packageRoot = dirname(__dirname);
9
+ const pythonPath = process.env.HOSTBRIDGE_PYTHON || process.env.SERVER_CONTROL_MCP_PYTHON || 'python3';
10
+ const bundledSrc = join(packageRoot, 'src');
11
+ const moduleDir = join(bundledSrc, 'server_control_mcp');
12
+ const docsUrl = 'https://www.npmjs.com/package/@neoline/hostbridge';
13
+
14
+ if (!existsSync(moduleDir)) {
15
+ console.error(`hostbridge: bundled Python sources not found at ${moduleDir}`);
16
+ console.error(` This npm package wraps a Python MCP core. See: ${docsUrl}`);
17
+ process.exit(1);
18
+ }
19
+
20
+ const env = { ...process.env };
21
+ env.PYTHONPATH = env.PYTHONPATH ? `${bundledSrc}${delimiter}${env.PYTHONPATH}` : bundledSrc;
22
+
23
+ const child = spawn(pythonPath, ['-m', 'server_control_mcp', ...process.argv.slice(2)], {
24
+ stdio: 'inherit',
25
+ env,
26
+ });
27
+
28
+ child.on('error', (error) => {
29
+ if (error.code === 'ENOENT') {
30
+ console.error(`hostbridge: Python interpreter '${pythonPath}' was not found.`);
31
+ console.error(' Install Python 3.11+ (https://www.python.org/downloads/)');
32
+ console.error(' or set HOSTBRIDGE_PYTHON to an absolute python3 path.');
33
+ } else {
34
+ console.error(`hostbridge: failed to start Python runtime: ${error.message}`);
35
+ console.error(` See: ${docsUrl}`);
36
+ }
37
+ process.exit(1);
38
+ });
39
+
40
+ child.on('exit', (code, signal) => {
41
+ if (signal) {
42
+ process.kill(process.pid, signal);
43
+ return;
44
+ }
45
+ if (code === 2) {
46
+ // exit code 2 is reserved by the Python runtime guard for missing dependencies.
47
+ // The actionable install command was already printed to stderr by Python.
48
+ console.error(`\nhostbridge: runtime dependency check failed. See: ${docsUrl}`);
49
+ }
50
+ process.exit(code ?? 0);
51
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import './hostbridge.js';
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "hostbridge": {
4
+ "command": "npx",
5
+ "args": ["-y", "hostbridge"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,7 @@
1
+ [mcp_servers.hostbridge]
2
+ command = "npx"
3
+ args = ["-y", "hostbridge"]
4
+
5
+ # Optional: use a project-specific host file instead of ~/.hostbridge/hosts.json
6
+ # [mcp_servers.hostbridge.env]
7
+ # HOSTBRIDGE_HOSTS_FILE = "/absolute/path/to/hosts.json"
@@ -0,0 +1,11 @@
1
+ {
2
+ "hosts": [
3
+ {
4
+ "id": "gpu",
5
+ "label": "GPU server",
6
+ "ssh": {
7
+ "host": "alice@gpu.example.com"
8
+ }
9
+ }
10
+ ]
11
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@neoline/hostbridge",
3
+ "version": "0.2.0",
4
+ "description": "HostBridge MCP server for persistent allowlisted SSH and shell sessions.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "mcp",
9
+ "model-context-protocol",
10
+ "ssh",
11
+ "pty",
12
+ "remote-control",
13
+ "ai-agent"
14
+ ],
15
+ "homepage": "https://www.npmjs.com/package/@neoline/hostbridge",
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "bin": {
20
+ "hostbridge": "bin/hostbridge.js",
21
+ "server-control-mcp": "bin/server-control-mcp.js"
22
+ },
23
+ "files": [
24
+ "bin/",
25
+ "src/server_control_mcp/**/*.py",
26
+ "pyproject.toml",
27
+ "README.md",
28
+ "LICENSE",
29
+ "examples/"
30
+ ],
31
+ "scripts": {
32
+ "test:node": "node --test"
33
+ }
34
+ }
package/pyproject.toml ADDED
@@ -0,0 +1,89 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hostbridge"
7
+ version = "0.2.0"
8
+ description = "HostBridge MCP server for persistent allowlisted SSH and shell sessions."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "HostBridge contributors" },
15
+ ]
16
+ keywords = [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "ssh",
20
+ "pty",
21
+ "remote-control",
22
+ "ai-agent",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Environment :: Console",
27
+ "Intended Audience :: Developers",
28
+ "Intended Audience :: System Administrators",
29
+ "Operating System :: POSIX",
30
+ "Operating System :: MacOS",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Topic :: System :: Systems Administration",
36
+ "Topic :: Software Development :: Libraries",
37
+ ]
38
+ dependencies = [
39
+ "mcp>=1.0.0",
40
+ "pexpect>=4.9.0",
41
+ "cryptography>=42.0.0",
42
+ ]
43
+
44
+ [project.optional-dependencies]
45
+ dev = [
46
+ "pytest>=8.0.0",
47
+ "pytest-cov>=4.1.0",
48
+ "ruff>=0.6.0",
49
+ "mypy>=1.10.0",
50
+ ]
51
+
52
+ [project.scripts]
53
+ hostbridge = "server_control_mcp.server:main"
54
+
55
+ [tool.setuptools]
56
+ package-dir = { "" = "src" }
57
+
58
+ [tool.setuptools.packages.find]
59
+ where = ["src"]
60
+
61
+ [tool.pytest.ini_options]
62
+ pythonpath = ["src"]
63
+ testpaths = ["tests"]
64
+
65
+ [tool.ruff]
66
+ line-length = 120
67
+ target-version = "py311"
68
+ extend-exclude = ["docs", "build", "dist"]
69
+
70
+ [tool.ruff.lint]
71
+ select = [
72
+ "E", "F", "W", # pycodestyle / pyflakes
73
+ "I", # isort
74
+ "UP", # pyupgrade
75
+ "B", # bugbear
76
+ "C4", # comprehensions
77
+ "SIM", # simplify
78
+ ]
79
+ ignore = ["E501"] # line length handled by formatter
80
+
81
+ [tool.ruff.lint.per-file-ignores]
82
+ "tests/*" = ["E402", "B011"]
83
+
84
+ [tool.mypy]
85
+ python_version = "3.11"
86
+ ignore_missing_imports = true
87
+ warn_unused_ignores = true
88
+ warn_redundant_casts = true
89
+ files = ["src/server_control_mcp"]
@@ -0,0 +1,54 @@
1
+ """Persistent SSH control MCP server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import NoReturn
7
+
8
+ __all__ = ["__version__", "ensure_runtime"]
9
+ __version__ = "0.2.0"
10
+
11
+ # (import_name, pip_name) pairs for runtime dependency probing.
12
+ _REQUIRED_DEPENDENCIES = (
13
+ ("mcp", "mcp"),
14
+ ("pexpect", "pexpect"),
15
+ ("cryptography", "cryptography"),
16
+ )
17
+
18
+
19
+ def _missing_dependencies() -> list[str]:
20
+ """Return pip names of runtime dependencies that cannot be imported."""
21
+
22
+ missing: list[str] = []
23
+ for import_name, pip_name in _REQUIRED_DEPENDENCIES:
24
+ try:
25
+ __import__(import_name)
26
+ except ImportError:
27
+ missing.append(pip_name)
28
+ return missing
29
+
30
+
31
+ def ensure_runtime() -> None:
32
+ """Abort with an actionable error if required runtime deps are missing.
33
+
34
+ Called early from the server entrypoint and from the CLI so the user gets a
35
+ single, copy-pasteable fix instead of a deep ImportError traceback.
36
+ """
37
+
38
+ missing = _missing_dependencies()
39
+ if not missing:
40
+ return
41
+
42
+ install_command = "pip install " + " ".join(missing)
43
+ message = (
44
+ f"hostbridge: missing Python dependencies: {', '.join(missing)}\n"
45
+ f" Install them with: {install_command}\n"
46
+ " Or reinstall the package: pip install --force-reinstall hostbridge"
47
+ )
48
+ print(message, file=sys.stderr)
49
+ sys.exit(2)
50
+
51
+
52
+ def _abort_with_error(message: str) -> NoReturn:
53
+ print(message, file=sys.stderr)
54
+ sys.exit(2)
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from . import ensure_runtime
4
+
5
+
6
+ def main() -> None:
7
+ ensure_runtime()
8
+ # Imported lazily so a missing mcp/pexpect/cryptography dependency surfaces
9
+ # the friendly ensure_runtime() message instead of a raw ImportError.
10
+ from .server import main as server_main
11
+
12
+ server_main()
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()