@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 +21 -0
- package/README.md +350 -0
- package/bin/hostbridge.js +51 -0
- package/bin/server-control-mcp.js +2 -0
- package/examples/claude_desktop_config.json +8 -0
- package/examples/codex.config.toml +7 -0
- package/examples/hosts.example.json +11 -0
- package/package.json +34 -0
- package/pyproject.toml +89 -0
- package/src/server_control_mcp/__init__.py +54 -0
- package/src/server_control_mcp/__main__.py +16 -0
- package/src/server_control_mcp/cli.py +1178 -0
- package/src/server_control_mcp/hosts.py +243 -0
- package/src/server_control_mcp/manager.py +512 -0
- package/src/server_control_mcp/policy.py +187 -0
- package/src/server_control_mcp/secrets.py +54 -0
- package/src/server_control_mcp/server.py +249 -0
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
|
+
});
|
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()
|