Skip to main content

SSH & SFTP MCP Server (ssh-mcp-vn)

MCP server cho SSH/SFTP nhiều máy chủ, có durable job qua nProx Runner trên Windows/Linux và Windows agentless đáp ứng điều kiện detach. Bản 0.6.3 đã được phát hành trên PyPI.

Current support matrix (0.6.3)

Capability Windows Linux
Durable jobs Fresh compatible service preferred; safe complete-breakaway agentless fallback when permitted Service required; otherwise use legacy synchronous SSH explicitly, not automatic durable fallback
PTY Service-only native ConPTY on Windows 10 1809 / Server 2019+ Service-only delegated-cgroup terminal implementation; native Linux acceptance still unverified
Stdin / resize Service stdin and capability-gated ConPTY resize Service stdin and capability-gated PTY resize
Hard log quota / immutable artifacts Service only; agentless has neither guarantee Service only; legacy synchronous execution has neither guarantee
Verified transfers Alias-only scoped SFTP, independent hash readback Same; local paths always refer to the MCP host staging root
Health Read-only marker facts and independent checks; no executable/version command Same

runner=auto prefers the service; required refuses unavailable service; disabled never chooses it. A submitted job retains its owning mode: service loss never triggers agentless replay. See installation and rollback for private ACLs, absolute executables/spool, clean PATH, Windows SCM and systemd Delegate=yes/cgroup-v2 requirements. These are operator instructions, not evidence of a live install. SCM/systemd deployment, SSH disconnect/reconnect on real hosts, restart/reboot, native Linux containment/PTY, verified SFTP on live Windows/Linux servers and ChatGPT connector acceptance remain external gates. The MCP package and Hub upstream use 0.6.3; runner services on target hosts must still be installed separately. Runner protocol 1 remains compatible with an installed 0.6.0 service.

Verified file transfers (0.6.0)

sftp_upload and sftp_download use configured aliases only, with no inline credentials, file content or Base64. Both return {server_name,source_path,destination_path,bytes_transferred, sha256,verification}; verification is always sha256, even without an expected digest.

Input Contract
local_path Required nonempty POSIX-relative file path inside the MCP host's SFTP_STAGING_ROOT, not the caller device; upload source or download destination. Example: release/app.zip.
remote_path Required absolute file path using forward slashes, inside the alias's transfer roots. Examples: /srv/releases/app.zip, C:/releases/app.zip.
server_name Optional configured alias; default null selects the only configured server. Required with multiple aliases. Example: build.
expected_sha256 Optional 64-hex-digit source digest; default null. Recommended for downloaded job artifacts. Example: ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.
overwrite Optional boolean, default false (atomic no-clobber). True permits atomic replacement of an existing regular file.

Configure an absolute existing SFTP_STAGING_ROOT on the MCP host and optional per-alias allowed_transfer_roots, for example ["/srv/releases", "/srv/runner/jobs"] or ["C:/releases", "C:/nprox/jobs"]. An empty list allows only canonical remote login home and its descendants, not the whole filesystem. Parents must already exist: transfers do not create directories. Paths reject traversal, symlinks, junction/reparse traversal, devices and alternate data streams. Keep these roots private to trusted transfer/service accounts; SFTP metadata does not replace filesystem ACLs or protection against another privileged writer. Legacy file tools retain their compatibility behavior and do not acquire the new remote-root policy.

SFTP_MAX_FILE_SIZE is a positive byte limit, default 2147483648 (2 GiB), checked before and during streaming and independent temporary-file readback. Source bytes are incrementally hashed; a securely named exclusive sibling temporary file is independently SHA-256 checked before atomic publication. Remote overwrite requires the server's POSIX-rename extension; there is no delete-and-rename fallback. Local no-clobber uses a same-directory hard link and requires filesystem support. Neither operation replays automatically. On uncertain disconnect/publication, inspect the destination and named .part-* sibling before deciding what to do; temporary evidence is deliberately retained. Only definite pre-publication failures clean their validated temporary file. Source files remain untouched.

Example upload:

{"local_path":"release/app.zip","remote_path":"/srv/releases/app.zip","server_name":"build","overwrite":false}

For a binary or complete job log, obtain get_job_artifact, then pass its path and digest:

{"local_path":"logs/stdout.log","remote_path":"/srv/runner/jobs/job-0123456789abcdef0123456789abcdef/stdout.log","server_name":"build","expected_sha256":"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"}

Call sftp_download with this shape (replace the sample digest with the actual artifact digest). Create logs inside staging first. File bytes never enter MCP JSON. Existing sftp_upload_file, sftp_download_file and sftp_upload_staged_file remain available; staged-upload SHA-256 verification now uses SFTP readback instead of a remote sha256sum command.

Durable job API (0.6.3)

Capability order: nProx Runner tương thích/healthy → Windows agentless với complete Job Object breakaway. Linux agentless bị từ chối vì descendant có thể thoát process group; Linux không có runner vẫn gọi ssh_execute_command trực tiếp. start_job/execute_script không fallback ngầm sang lệnh đồng bộ. Native remote service/reboot acceptance chưa được xác minh; xem hướng dẫn runner operations trước khi triển khai. Windows có ancestor Job Object không cho breakaway cũng trả capability error có hướng xử lý.

Job tools chỉ nhận đúng configured server name, không nhận host/user/password/key. Display name có thể chứa khoảng trắng/Unicode; chỉ key của MCP server trên Hub mới bắt buộc là slug. Giữ tên target và spool ổn định để tìm lại job sau restart. Ví dụ policy (host minh họa):

{
  "name": "windows-build", "host": "windows.example.invalid", "user": "ops",
  "key_path": "C:/keys/ops_ed25519", "platform": "windows",
  "job_spool": "C:/nprox/jobs", "allowed_shells": ["powershell", "pwsh", "cmd"],
  "allowed_cwd_roots": ["C:/build workspace"],
  "executables": {"pwsh": "C:/Program Files/PowerShell/7/pwsh.exe"}, "runner": "auto"
}

Policy fields: platform defaults to auto (fixed read-only OS probes), job_spool defaults to .nprox-ssh/jobs, allowed_shells=null selects the resolved OS defaults, allowed_cwd_roots=[] allows only canonical remote login home and its subdirectories. Explicit roots replace that default; include the home path explicitly to retain access to it. executables={} uses platform defaults. Executable overrides must be absolute paths. runner=auto|disabled|required defaults to auto; required needs a fresh compatible exact-tree service marker. Use a dedicated account and private spool ACLs; do not let untrusted users modify request/script/state/launcher files.

Input contracts

start_job and execute_script have the same flat inputs. Types are strict; unknown fields fail.

Field Contract and directly usable example
shell Required enum powershell/pwsh/cmd/bash/sh subject to alias policy; "powershell". Linux durable execution requires the runner service.
script Required UTF-8 source, 1–1,000,000 characters; "Write-Output 'Việt Nam'". Multiline scripts are uploaded as files.
idempotency_key Required opaque string, 1–200 characters; "build-20260914-1". Same alias/key plus identical normalized request reuses; different content conflicts.
server_name Optional configured slug; null selects the only alias, required with multiple aliases; "windows-build".
cwd Optional absolute remote directory; default null uses canonical login home even when spool is elsewhere. Empty roots restrict cwd to home and its subdirectories. Required when roots are configured; "C:/build workspace". Symlinks/reparse traversal are rejected before publication and checked again by the child. Windows SFTP /C:/... paths are converted to native cwd.
env Optional flat string map; null means {}; {"CI":"true"}. Names match [A-Za-z_][A-Za-z0-9_]* and must be unique ignoring case on Windows. Values cannot contain NUL; Unicode/newlines are preserved. Encoded JSON ≤64 KiB.
command_timeout Integer seconds 1–86400, default 120; 900. Supervisor stops the owned process tree.
idle_timeout Integer seconds 1–86400, default null disables no-output timeout; 300.
pty Boolean default false; true requires a fresh service marker with pty=true, never agentless/legacy fallback. Initial terminal size 80x24; merged stdout/stderr appear in stdout. Windows uses native ConPTY; Linux uses a delegated cgroup terminal.

job_status and cancel_job require job_id matching job- plus 32 lowercase hex digits, for example job-0123456789abcdef0123456789abcdef; optional server_name follows the same rule. job_logs additionally accepts independent stdout_offset and stderr_offset integer byte positions ≥0 (both default 0; examples 4096 and 512) and per-stream limit 1–262144 bytes (default 65536; example 32768). Resume using returned next offsets to preserve complete UTF-8 codepoints. job_input adds required UTF-8 text 1–1,000,000 characters (example "yes\n") and integer sequence from 1 through 10^20−1 (example 1). It requires service ownership and a freshly verified compatible marker with stdin=true (and pty=true for PTY jobs); agentless jobs reject input without storing it. For canonical PTYs, send "\u0003" for Ctrl-C or "\u0004" on an empty line for Ctrl-D/EOF; raw-mode applications interpret these bytes themselves. Ordinary piped stdin has no public EOF flag. job_resize takes required job_id, columns and rows (integers 1–1000), and sequence (integer 1 through 10^20−1, starting at 1 independently of stdin), plus optional server_name. Example: {"job_id":"job-0123456789abcdef0123456789abcdef","columns":100,"rows":30,"sequence":1}. It accepts only nonterminal service PTY jobs with a fresh pty=true marker. Retry identical dimensions only; gaps wait for earlier sequences. A returned status confirms durable publication, not application; the worker records the last applied sequence/dimensions in pty-state.json. Failed acknowledgement interrupts the job, never replays a possibly applied resize.

{"shell":"powershell","script":"Write-Output 'Việt Nam'; [Console]::Error.WriteLine('warning'); exit 0","idempotency_key":"build-20260914-1","server_name":"windows-build","cwd":"C:/build workspace"}

Call start_job with this object for background execution, or execute_script to wait. Then call job_status with the returned job_id; read job_logs repeatedly with returned next offsets. Call cancel_job with that same job ID to stop its identity-checked owning tree. Cancelling an already terminal job returns its current status. Cancellation is never performed by process name.

Result contracts and recovery

  • start_job: {job_id, state, mode, reused}. Mode is service or agentless; there are no submission timestamps or negotiated-capability fields in this release. queued is not proof of launch.
  • job_status/cancel_job: {job_id,state,mode,exit_code,started_at,finished_at,stdout_size, stderr_size,pid,pgid,process_identity,error_code}. Unknown exit/timing/identity fields are null; timestamps are UTC ISO-8601, sizes are bytes, and process identity is an opaque OS creation value. Public runner error_code is null or one of RUNNER_INTERRUPTED, RUNNER_INVALID_JOB, RUNNER_PUBLICATION_FAILED and RUNNER_LOG_QUOTA_EXCEEDED; arbitrary stored diagnostics are not exposed.
  • job_logs: {stdout,stderr,next_stdout_offset,next_stderr_offset,stdout_eof,stderr_eof}. EOF is a snapshot, not completion; a running job can append later. Pagination is unchanged.
  • job_log_preview(job_id,limit=65536,server_name=null) returns {stdout,stderr,stdout_size, stderr_size,truncated}. Limit is aggregate emitted TextContent.text JSON UTF-8 bytes, including indentation, metadata, escaping and omission markers; integer 256–262144. MCP framing and the duplicate structuredContent object are excluded. Both streams initially share equally, then unused space goes to stdout before stderr. Heads/tails never overlap or split UTF-8 characters. Invalid UTF-8 is an actionable error, never replacement text. This is a snapshot, not a resumable page; use job_logs for complete sequential text.
  • get_job_artifact(job_id,stream,server_name=null), with stream stdout or stderr, returns {remote_path,size,sha256} only for terminal service logs after all output pumps close. SHA-256 covers exactly the retained immutable bytes, including binary logs. No artifact bytes enter MCP JSON. Metadata is atomically cached in result.json; proven stopped-owner recovery can backfill older completed service logs. Agentless artifacts are unsupported; use legacy log access. Download with sftp_download, passing this remote_path and sha256 as expected_sha256; the artifact directory must be within the alias's allowed_transfer_roots.
  • execute_script: {exit_code,timed_out,duration_ms,stdout,stderr,truncated,job_id,error_code}. It polls every 250 ms for at most command_timeout + 30 seconds after submission, including reconnect and collection time. Each stream is capped at 262144 bytes; use job_logs for the remainder.

States are queued, running, succeeded, failed, timed_out, cancelled, interrupted. Native exit 0 succeeds even with warning stderr unless a runner error code is present; nonzero exit is structured failure, not an MCP protocol error. Null native exit can occur when execution never started or was interrupted. Definite Windows detach exit 78 is recorded separately in immutable launch-refusal.json as AGENTLESS_DETACH_UNSUPPORTED; status is failed with null exit/start and no output. Repeat submission returns the recorded capability guidance. A transport loss never invents a refusal or native result.

SSH connect timeout uses SSH_CONNECT_TIMEOUT (default 10 seconds); keepalive is 15 seconds with three unanswered probes. Read-only status/log/result/refusal recovery and OS probes use at most four attempts, sleeping 0.25/0.5/1 second plus 0–0.1-second jitter between failures. Authentication, permission and invalid-data errors are not retried. The execution wait deadline still applies. Writes, launch, input and cancel are not generically retried.

After ambiguous submission, reconnect inspects the deterministic job directory and request hash. An existing submission may be returned as reused; it is never launched a second time. An ambiguous launch remains an actionable tool error after inspection. If publication left queued without a launched supervisor, it can remain queued: inspect the spool and resolve operationally, never change keys or delete identity data merely to retry an uncertain write. Missing data after reconnect is not an instruction to replay automatically. Keep finished identity records for as long as keys can be retried; deleting the spool removes that protection. Service startup recovers only the prior owner proven stopped by its OS-released exclusive spool lock, never heartbeat age alone.

Windows scripts use UTF-8 BOM (cmd receives a separate BOM-free .cmd file). The supervisor starts children suspended, assigns a private Job Object, retains ownership after the leader exits and drains the tree on timeout/cancel. Environment starts from required Windows variables and a sanitized PATH excluding OpenAI/Codex/bin; configured executable overrides use absolute paths. Script and env are retained in the remote spool: do not pass secrets unless that storage is approved.

Runner-service captures enforce SSH_RUNNER_MAX_JOB_LOG_BYTES from the service host environment: default 1073741824 (1 GiB), decimal integer 0–9223372036854775807. This is a hard retained aggregate stdout+stderr byte quota per job (PTY output is merged stdout), not a filesystem or temporary-file quota. Owned concurrent pumps retain no more than that budget, hash retained bytes incrementally, record quota exhaustion, cancel the exact tree once and drain/close remaining pipes. Zero permits empty output only; exactly filling the budget is allowed until another byte arrives. Exhaustion publishes state=failed, error_code=RUNNER_LOG_QUOTA_EXCEEDED, truncated=true, preserving any native exit including zero. Quota failure takes precedence over concurrent cancellation/timeout. Inspect retained artifacts and increase the service quota before submitting a genuinely new job; do not change an existing key merely to replay uncertain work. A quota can cut binary or UTF-8 bytes; metadata still works, while text reads reject incomplete/invalid UTF-8. Direct/agentless execution retains legacy behavior without this service hard quota. There is no rotation or automatic cleanup; operators must still provision disk capacity, filesystem quotas and retention. Keep spool logs immutable after completion; external modifications invalidate cached artifact evidence.

Windows execution normalizes the effective PATHEXT after applying env overrides: .COM, .EXE, .BAT, and .CMD are always present. Additional extensions matching \.[A-Za-z0-9]+ are preserved; entries are trimmed, uppercased, and deduplicated, and empty/malformed entries are discarded. This also applies to empty or mixed-case PathExt overrides, so absolute native commands remain executable.

Cmd's UTF-8 label offsets can silently skip commands. Non-ASCII cmd scripts therefore reject label definitions, GOTO, CALL :label, and dynamic CALL targets containing %/! before submission or launch. This is conservative lexical validation (including escaped tokens), not a batch parser; comments/text resembling these constructs may also be refused. Use PowerShell/pwsh or an ASCII-only batch script for label/subroutine flow. Plain Unicode filenames and static CALL other.cmd remain supported. Derived .cmd files use CRLF and initialize UTF-8 through the quoted absolute %SystemRoot%\System32\chcp.com, independent of PATH; initialization failure exits 78 before user content. Windows SystemRoot is protected from request env overrides. Stored source/hashes do not change.

The existing SSH connector currently sets known_hosts=None, so it does not verify host keys. Deploy only within a trusted connection boundary; this release does not change that legacy setting. Legacy ssh_execute_command is deprecated for supported durable workflows but retained through at least this release window. Its raw-shell quoting, synchronous lifetime and text output remain.

Acceptance and evaluations

The legacy agentless integration module contacts SSH only if SSH_JOB_TEST_CONFIG is set. It must point to a JSON object with disposable:true and windows/linux entries, each containing server (the config shape above with explicit platform and allowed cwd roots) and cwd (an existing path with spaces). Run pytest tests/integration/test_remote_jobs.py -v only against disposable hosts; the Windows durability case takes at least ten minutes. That module tests the no-runner contract; use separate fixtures from the runner acceptance module below. Linux quoting is checked through legacy execution. Local Git disconnect acceptance pushes only to a temporary local bare repository and verifies one pre-receive hook invocation.

evaluations/ssh_jobs_read_only.xml contains ten independent status/log questions over the fixed job_read_only_fixture.json dataset. tests/test_job_evaluation.py verifies the answers through actual MCP tool calls against in-memory SFTP, including 4-byte UTF-8 pagination; no remote fixture is provisioned. For an LLM evaluation, provision these immutable fixture states/logs under a dedicated evaluation alias/spool beforehand and allow only job_status/job_logs. Job IDs are given in the fixture. The answer verification test is not an LLM quality score.

Hướng dẫn cài đặt

Chạy qua UVX (Được khuyến nghị)

Bạn có thể chạy trực tiếp server này qua uvx mà không cần phải tải hay cài đặt thủ công:

uvx ssh-mcp-vn==0.6.3

Các tính năng chính

  • Hỗ trợ Multi-host: Tương tác với nhiều máy chủ Windows/Linux mà không cần chạy nhiều instance.
  • Remote execution runner: Ưu tiên service trên máy đích; Windows có agentless fallback an toàn, Linux không có runner vẫn dùng tool đồng bộ legacy một cách tường minh.
  • Durable jobs: start_job, status/log/input/resize/cancel, idempotency và reconnect không chạy lại thao tác chưa xác định kết quả.
  • Thực thi qua SSH: Chạy lệnh bash/shell tùy ý trên máy chủ từ xa.
  • Quản lý thư mục: Dễ dàng tạo cấu trúc thư mục từ xa (hoạt động giống lệnh mkdir -p).
  • Tải lên qua SFTP: Đẩy file local của MCP process hoặc file staging trên MCP Hub lên máy chủ từ xa.
  • Tải về qua SFTP: Tải file từ máy chủ từ xa về máy tính.
  • Liệt kê file bằng SFTP: Xem danh sách các file trong một thư mục bất kỳ trên máy chủ.

Cấu hình (Khuyên dùng qua File JSON)

Vì mảng cấu hình có thể khá dài và việc viết chuỗi JSON dính liền với các dấu \ (escape) trong cấu hình môi trường sẽ phức tạp, bạn nên tách cấu hình ra một file JSON riêng biệt.

Bước 1: Tạo một file có tên ssh_servers.json ở bất kỳ đâu trên máy của bạn (ví dụ C:\Users\BiNgo\.codex\ssh_servers.json) và điền danh sách các server dạng mảng thuần túy.

Ưu tiên dùng SSH key qua key_path và user vận hành riêng có sudo. Không nên để root + password plaintext làm cấu hình mặc định.

[
  {
    "name": "toppion-linux",
    "host": "192.168.1.100",
    "user": "ops",
    "key_path": "C:\\Users\\BiNgo\\.ssh\\toppion_ops_ed25519",
    "port": 22
  },
  {
    "name": "toppion-registry",
    "host": "192.168.1.101",
    "user": "ops",
    "key_path": "C:\\Users\\BiNgo\\.ssh\\toppion_ops_ed25519"
  },
  {
    "name": "toppion-master",
    "host": "192.168.1.102",
    "user": "ops",
    "key_path": "C:\\Users\\BiNgo\\.ssh\\toppion_ops_ed25519"
  }
]

Bước 2: Cập nhật mcp_config.json của ArcticFactory chỉ truyền vào biến SSH_CONFIG_FILE trỏ đến file đó:

{
  "mcpServers": {
    "ssh-server": {
      "command": "uvx",
      "args": [
        "ssh-mcp-vn==0.6.3"
      ],
      "env": {
        "SSH_CONFIG_FILE": "C:\\Users\\NamHT\\ssh_servers.json"
      }
    }
  }
}

Lấy danh sách server từ NProx API

Khi inventory được quản lý tập trung, truyền --url-host và đặt token trong biến môi trường SSH_URL_HOST_TOKEN. URL phải là HTTPS; token không được đưa vào args, log hoặc output tool.

{
  "mcpServers": {
    "ssh-server": {
      "command": "uvx",
      "args": [
        "ssh-mcp-vn==0.6.3",
        "--url-host",
        "https://nprox.dev/api/external/devops/servers"
      ],
      "env": {
        "SSH_URL_HOST_TOKEN": "<NPROX_API_TOKEN>",
        "SSH_REMOTE_POLICY": "{\"windows-build\":{\"platform\":\"windows\",\"job_spool\":\".nprox-ssh/jobs\",\"allowed_cwd_roots\":[\"C:/nprox/work\"],\"runner\":\"required\"}}"
      }
    }
  }
}

Endpoint phải trả về object có mảng servers; mỗi item cần name, host, port và username. Nếu item không có password/key_path, MCP gọi endpoint kế bên /connection?name=<URL-encoded-name> bằng cùng Bearer token và chỉ giữ credential trong process; credential không xuất hiện trong tool schema, log hoặc kết quả.

SSH_REMOTE_POLICY là object JSON theo tên server, chỉ cho phép các field không chứa secret: platform, job_spool, allowed_shells, allowed_cwd_roots, allowed_transfer_roots, executables, runner. Nó không thể thay host, user, port hoặc credential. Policy của MCP và tham số cài Runner phải dùng cùng job_spool và allowed_cwd_roots.

Thêm server KHÔNG dùng file JSON (nhập thẳng)

Nếu chỉ có 1 server và không muốn tạo file cấu hình hay escape chuỗi JSON, bạn có thể khai báo từng biến env riêng trong mcp_config.json:

Dạng 1 — Một server (từng biến env riêng, khuyên dùng):

{
  "mcpServers": {
    "ssh-server": {
      "command": "uvx",
      "args": [
        "ssh-mcp-vn==0.6.3"
      ],
      "env": {
        "SSH_NAME": "production",
        "SSH_HOST": "192.168.1.10",
        "SSH_USER": "ops",
        "SSH_PORT": "22",
        "SSH_KEY_PATH": "~/.ssh/toppion_ops_ed25519"
      }
    }
  }
}

Các biến hỗ trợ: SSH_NAME (bắt buộc), SSH_HOST (bắt buộc), SSH_USER, SSH_PORT (mặc định 22), SSH_KEY_PATH, SSH_PASSWORD.

Dạng 2 — Nhiều server (mảng JSON):

{
  "mcpServers": {
    "ssh-server": {
      "command": "uvx",
      "args": [
        "ssh-mcp-vn==0.6.3"
      ],
      "env": {
        "SSH_SERVERS": "[{\"name\": \"production\", \"host\": \"192.168.1.10\", \"user\": \"ops\", \"key_path\": \"~/.ssh/toppion_ops_ed25519\"}, {\"name\": \"staging\", \"host\": \"192.168.1.11\", \"user\": \"ops\", \"key_path\": \"~/.ssh/toppion_ops_ed25519\"}]"
      }
    }
  }
}

Thứ tự ưu tiên: SSH_URL_HOST > SSH_CONFIG_FILE > SSH_SERVERS > các biến SSH_NAME/SSH_HOST riêng lẻ. Nếu chỉ có 1 server, dùng Dạng 1 cho gọn, không cần escape JSON.

Hoặc chạy trực tiếp qua CLI với tham số --servers (dùng để test nhanh, hỗ trợ cả object đơn lẫn array):

uvx ssh-mcp-vn==0.6.3 --servers '{"name": "production", "host": "192.168.1.10", "user": "ops", "key_path": "~/.ssh/toppion_ops_ed25519"}' --smoke-test production

Các trường trong từng block JSON cấu hình

Mỗi object của một server hỗ trợ các trường sau:

  • name (bắt buộc): Tên gọi riêng cho server này (VD: "production", "staging") để giúp công cụ AI nhận biết server muốn thao tác.
  • host (bắt buộc): Địa chỉ IP hoặc Hostname của máy chủ SSH.
  • user (bắt buộc): Tên đăng nhập (username).
  • port (tùy chọn): Cổng SSH, mặc định là 22.
  • key_path (khuyên dùng): Đường dẫn tuyệt đối đến file khóa bí mật (private key).
  • password (tùy chọn, không khuyên dùng): Mật khẩu đăng nhập. Chỉ nên dùng tạm khi chưa chuyển xong sang SSH key.

Kiểm tra cấu hình và smoke test

Sau khi sửa ssh_servers.json, hãy restart/reload Codex MCP trước khi kết luận lỗi config vì MCP server đọc cấu hình khi process khởi động.

Kiểm tra config đã được đọc đúng và không in secret:

uvx ssh-mcp-vn==0.6.3 --config-file "C:\Users\BiNgo\.codex\ssh_servers.json" --validate-config

Chạy smoke test chuẩn cho toàn bộ server:

uvx ssh-mcp-vn==0.6.3 --config-file "C:\Users\BiNgo\.codex\ssh_servers.json" --smoke-test all

Smoke test dùng lệnh:

hostname && whoami && pwd && uptime

Nếu đang làm trực tiếp trong source repo, có thể dùng script:

python3 uvx/ssh/scripts/smoke_test.py --config-file "C:\Users\BiNgo\.codex\ssh_servers.json" --server all

Checklist vận hành ngắn:

  1. Dùng alias MCP thống nhất: toppion-linux, toppion-registry, toppion-master.
  2. Không phụ thuộc shell alias; MCP chỉ dùng name trong ssh_servers.json.
  3. Ưu tiên key_path; hạn chế password plaintext.
  4. Dùng user vận hành riêng có sudo; chỉ dùng root khi thật cần.
  5. Sửa config xong thì restart/reload Codex MCP rồi chạy smoke test.

Upload file từ Windows/macOS qua MCP Hub Linux

Khi MCP chạy trên server B, local_path là đường dẫn trên B, không phải đường dẫn Windows/macOS. Với file tới 1 GB, dùng hai chặng SFTP; không truyền base64 qua MCP:

Windows/macOS --OpenSSH SFTP--> /srv/ssh-mcp/inbox trên B --MCP SFTP--> server A

Cấu hình staging trên B

Tạo user chỉ dùng SFTP và một group mà service account chạy MCP có quyền đọc. Ví dụ dưới đây dùng mcp-upload làm user nhận file và ssh-mcp-readers làm group đọc:

sudo groupadd --system ssh-mcp-readers
sudo useradd --system --home-dir /inbox --shell /usr/sbin/nologin mcp-upload
sudo usermod -aG ssh-mcp-readers <mcp-service-user>
sudo mkdir -p /srv/ssh-mcp/inbox
sudo chown root:root /srv/ssh-mcp
sudo chmod 0755 /srv/ssh-mcp
sudo chown mcp-upload:ssh-mcp-readers /srv/ssh-mcp/inbox
sudo chmod 2750 /srv/ssh-mcp/inbox

Thay <mcp-service-user> bằng user Linux thực sự chạy MCP Hub. Cấu hình public key của mcp-upload bằng cơ chế AuthorizedKeysFile của hệ điều hành; không copy private key lên B.

Thêm block tương đương vào sshd_config, kiểm tra bằng sudo sshd -t, rồi reload SSH:

Match User mcp-upload
    ChrootDirectory /srv/ssh-mcp
    ForceCommand internal-sftp -d /inbox
    PasswordAuthentication no
    PubkeyAuthentication yes
    AllowTcpForwarding no
    X11Forwarding no
    PermitTunnel no

Cấu hình process MCP trên B:

SFTP_STAGING_ROOT=/srv/ssh-mcp/inbox
SFTP_MAX_FILE_SIZE=2147483648

Đặt quota/cảnh báo dung lượng cho B. Dùng systemd-tmpfiles hoặc cleanup job của hệ điều hành để xóa file staging và file .uploading quá 24 giờ.

Windows 10/11 PowerShell

Kiểm tra OpenSSH Client; chỉ cần chạy lệnh cài đặt bằng PowerShell Administrator nếu trạng thái là NotPresent:

Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Client*'
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
sftp -i "$env:USERPROFILE\.ssh\id_ed25519" mcp-upload@server-b

Trong prompt sftp, upload vào tên .uploading; reput tiếp tục file dở nếu mạng đứt. Khi hoàn tất mới rename thành tên ready:

sftp> mkdir release
sftp> reput "C:/Users/Nam/Downloads/app.tar.gz" release/app.tar.gz.uploading
sftp> rename release/app.tar.gz.uploading release/app.tar.gz

Lấy SHA-256 trên Windows nếu cần xác minh end-to-end:

(Get-FileHash -Algorithm SHA256 "C:\Users\Nam\Downloads\app.tar.gz").Hash.ToLower()

macOS

sftp -i "$HOME/.ssh/id_ed25519" mcp-upload@server-b
sftp> mkdir release
sftp> reput "/Users/nam/Downloads/app.tar.gz" release/app.tar.gz.uploading
sftp> rename release/app.tar.gz.uploading release/app.tar.gz
shasum -a 256 "/Users/nam/Downloads/app.tar.gz"

mkdir release chỉ cần chạy lần đầu. Windows và macOS đều dùng dấu / cho đường dẫn remote trên B. Không truyền đường dẫn C:/Users/... hoặc /Users/... vào MCP.

Relay file staging từ B sang A

Tool call giống nhau trên Windows và macOS:

sftp_upload_staged_file
  staged_path: release/app.tar.gz
  remote_path: /opt/releases/app.tar.gz
  server_name: server-a
  expected_sha256: <64-hex-digits>  # tùy chọn
  overwrite: false

Tool chỉ nhận đường dẫn tương đối bên trong SFTP_STAGING_ROOT, từ chối symlink và file còn đuôi .uploading. File được upload lên A bằng tên .part-*, kiểm tra kích thước/SHA-256 rồi mới rename sang tên thật. Mặc định không ghi đè file đã tồn tại và luôn giữ file staging để retry/cleanup sau.

Fixed disposable evaluation fixture

Mười câu hỏi read-only trong evaluations/ssh_staged_relay_read_only.xml dùng đúng ba file text dưới đây trên một target dùng riêng cho evaluation. MCP không tự tạo fixture này; không đặt nó lên production.

File Nội dung cố định
/srv/ssh-mcp-evaluation/manifests/windows.txt platform=windows, version=3.0.0, arch=x64, artifact=agent-3.0.0-win-x64.zip, bytes=1048576, sha256= + 64 ký tự 1
/srv/ssh-mcp-evaluation/manifests/macos.txt platform=macos, version=3.0.0, arch=arm64, artifact=agent-3.0.0-macos-arm64.tar.gz, bytes=524288, sha256= + 64 ký tự 2
/srv/ssh-mcp-evaluation/incoming/status.txt temporary=.app-3.1.0.tar.gz.part-deadbeef, bytes=262144, state=incomplete

Evaluation chỉ dùng sftp_list_directory và các lệnh đọc như find, stat, cat; không upload, rename, xóa hoặc chmod.

Danh sách công cụ (Tools)

  • start_job, execute_script: Chạy script UTF-8 với shell, cwd, env, timeout và idempotency_key; không nhét PowerShell vào raw command string.
  • job_status, job_logs, job_log_preview, get_job_artifact: Theo dõi job và đọc output lớn theo offset hoặc artifact SHA-256.
  • job_input, job_resize, cancel_job: Điều khiển stdin/PTY và dừng đúng process tree của job.
  • runner_health: Kiểm tra SSH, runner, account, disk và executable paths mà không chạy tool.
  • sftp_upload, sftp_download: Truyền file có phạm vi cho phép và xác minh SHA-256.
  • ssh_execute_command: Chạy một lệnh shell trên máy Linux từ xa.
  • ssh_create_folder: Tạo thư mục từ xa.
  • sftp_upload_file: Chuyển một file từ local lên máy chủ.
  • sftp_upload_staged_file: Chuyển an toàn một file ready trong staging root của MCP Hub lên máy chủ, hỗ trợ kiểm tra SHA-256 và atomic rename.
  • sftp_download_file: Lấy một file từ máy chủ về local.
  • sftp_list_directory: Liệt kê tất cả file trong một thư mục từ xa.
  • list_ssh_servers: Liệt kê server đã cấu hình. Chỉ xuất hiện khi cấu hình nhiều hơn 1 server.

Khi chỉ cấu hình 1 server, tham số server_name của các tool là tùy chọn (bỏ trống cũng được, hệ thống tự dùng server duy nhất). Khi có nhiều server, server_name là bắt buộc để biết thao tác trên máy nào.

Kết nối trực tiếp khi chưa lưu credential (Inline)

Các tool SSH/SFTP legacy nhận thêm host, user, port, password, key_path; truyền host + user để kết nối khi chưa lưu credential. Các job tool 0.6.0 chỉ nhận configured alias.

Thứ tự ưu tiên resolve server:

  1. Có host → dùng inline connection (ghi đè server_name).
  2. Không có host nhưng có server_name → dùng server đã cấu hình.
  3. Không có cả hai → fallback về server duy nhất đã cấu hình (hoặc báo lỗi nếu chưa cấu hình).
ssh_execute_command
  command: df -h
  host: 192.168.1.50
  user: ops
  port: 2222
  key_path: ~/.ssh/toppion_ops_ed25519

Lưu ý bảo mật: credential inline chỉ tồn tại trong tool call, không được ghi vào config hay log. Ưu tiên key_path; hạn chế truyền password plaintext qua MCP.

Hướng dẫn sử dụng (Usage)

Sau khi server đã được khai báo, bạn thao tác qua server_name — giá trị phải khớp chính xác trường name trong cấu hình. Ví dụ với server có "name": "toppion-linux":

1. Chạy lệnh shell trên server

ssh_execute_command
  server_name: toppion-linux
  command: df -h

Chỉ có 1 server? Bỏ qua server_name:

ssh_execute_command
  command: df -h

2. Tạo thư mục từ xa

ssh_create_folder
  server_name: toppion-linux
  remote_path: /var/www/backend/logs

3. Truyền file qua SFTP

Upload file từ máy local lên server:

sftp_upload_file
  server_name: toppion-linux
  local_path: /Users/namho/config/app.yml
  remote_path: /etc/app/config.yml

Download file từ server về máy local:

sftp_download_file
  server_name: toppion-linux
  remote_path: /var/log/nginx/error.log
  local_path: /Users/namho/downloads/error.log

Liệt kê file trong một thư mục trên server:

sftp_list_directory
  server_name: toppion-linux
  remote_path: /var/log

4. Xem danh sách server đã cấu hình (chỉ có khi cấu hình nhiều server)

list_ssh_servers

Mẹo: Muốn thao tác nhanh với nhiều server, hãy đặt name ngắn gọn và ổn định (VD: production, staging, toppion-linux) và dùng đúng alias đó trong mọi lời gọi tool.

Changelog

0.6.0 - 2026-09-15

New

  • Thêm runner-first durable jobs, structured results, idempotent reconnect, stdin/cancel và PTY service trên Windows/Linux.
  • Thêm runner health, log preview/artifacts và SFTP transfer có scope + SHA-256.

Changed

  • PowerShell/pwsh job nhận script UTF-8 riêng, không đi qua raw SSH command quoting.
  • runner=auto ưu tiên service; Windows fallback agentless khi detach an toàn, không replay khi mất kết nối. Linux dùng legacy synchronous tool khi chưa có runner.

0.4.0 - 2026-08-17

New

  • Thêm inline connection: mọi tool nhận thêm host, user, port, password, key_path để kết nối thẳng khi chưa có credential lưu sẵn.

0.2.0 - 2026-08-10

Breaking

  • Xoá tool ssh_smoke_test và ssh_smoke_test_all khỏi MCP (vẫn dùng được qua CLI --smoke-test).

Changed

  • Chỉ cấu hình 1 server → server_name là tùy chọn ở mọi tool; list_ssh_servers chỉ xuất hiện khi có nhiều server.

0.1.8 - 2026-08-10

New

  • SSH_SERVERS, --servers và file JSON giờ chấp nhận cả object đơn {...} lẫn array [{...}], không bắt buộc bọc mảng khi chỉ có 1 server.

0.1.7 - 2026-08-10

Fixed

  • Khóa mcp>=1.2.0,<2 để tránh uvx resolve nhầm lên mcp 2.0.0, bản này đã bỏ mcp.server.fastmcp khiến server crash với ModuleNotFoundError: No module named 'mcp.server.fastmcp'.

0.1.5 - 2026-07-05

New

  • list_ssh_servers: Xem danh sách server đã cấu hình với thông tin auth đã sanitize, không in password.
  • ssh_smoke_test: Kiểm tra nhanh một server bằng lệnh hostname && whoami && pwd && uptime.
  • ssh_smoke_test_all: Kiểm tra nhanh toàn bộ server trong ssh_servers.json.
  • CLI --validate-config: Kiểm tra config và cảnh báo vận hành mà không cần mở MCP client.

Improved

  • Khuyến nghị cấu hình bằng key_path và user vận hành riêng thay vì mặc định root + password plaintext.
  • Publish scripts dùng UV_PUBLISH_TOKEN thay vì lưu PyPI token trong source.
  • key_path hỗ trợ ~ và biến môi trường.
  • SSH command có timeout mặc định để tránh treo lâu khi server không phản hồi.

Fixed

  • Sửa tương thích MCP SDK bằng cách bỏ tham số description không được hỗ trợ trong FastMCP(...).
  • ssh_create_folder quote remote path trước khi chạy mkdir -p.

Xem lịch sử đầy đủ tại CHANGELOG.md.

License

MIT

Runner process ownership

The source tree includes runner selection, service packaging and runner_health(server_name=null). Actual service installation and remote acceptance remain unverified. Windows development installs use uv sync --extra dev --extra runner-windows.

runner=auto prefers a compatible protocol-1 capabilities.json heartbeat at most 30 seconds old with exact-tree ownership; required refuses missing/stale/unsafe runners and disabled never selects them. SFTP capability reads cache at most five monotonic seconds per alias/host/account/spool. Future timestamps beyond five seconds fail validation. Unsupported Windows detachment and Linux without a healthy runner retain actionable errors and legacy synchronous guidance. An unreadable capability marker makes the runner unavailable; transport loss or denied job writes do not trigger fallback. Required mode reports marker access/permissions errors actionably.

Mode is committed inside the atomic submission, before ready becomes visible. The worker claims only queued service jobs; an appearing service cannot steal an agentless job. If the service stops after submission, its job remains queued for restart, without fallback/relaunch. Existing keys retain their recorded owner even when runner policy or availability changes.

job_input persists immutable UTF-8 sequence files for nonterminal service jobs; identical retries reuse bytes and different text conflicts. Sequence numbers start at 1 and stay below 10^20. The EOF marker stays internal; PTY users can send terminal Ctrl-D bytes as described above. Service cancellation publishes the validated cancel.request control once and polls for terminal state/result for at most ten seconds. Lost acknowledgements reconnect for inspection only. Agentless stdin remains unsupported; terminal cancellation is a no-op. Input rechecks the current healthy marker with stdin=true before writing, bypassing the cache. A valid cancellation present before worker launch prevents process creation, including cancellation during the final capability check. PTY is service-only and rechecks current capability before launch. Linux uses openpty, a session and controlling terminal after the existing delegated-cgroup gate; exact cancellation still uses cgroup.kill, not process groups. Terminal output (including ANSI escapes, echo and terminal line endings) is merged into stdout; stderr is empty. Buffered output drains before the native exit is published, with a five-second terminal EOF drain deadline after the tree stops; drain/protocol errors interrupt rather than masquerading as native success. Spool disk I/O must still complete before handles are released. Ctrl-C targets the terminal foreground group; use cancel_job for the entire exact owned tree. JobRequest identity/default hashes are unchanged.

PTY is advertised only after a real contained terminal child consumes input and produces expected output with native exit 0. Windows uses native ConPTY on Windows 10 1809 / Server 2019 or newer: the client is created suspended, assigned to the private no-breakaway Job Object, then resumed; output is drained on its own thread before ConPTY closes. No pywinpty dependency or PATH change is required. Native Linux tests require the existing opt-in NPROX_TEST_CGROUP_ROOT disposable delegated root; without it (including this Windows development host), Linux deployment acceptance remains unverified.

Health preserves {protocol,ssh,healthy,capabilities,reason} and adds optional runner, account, disk, powershell_version check objects and an executables map. Each check is {ok,detail,value}: ok=true means the fact is available, false means unavailable (or zero free disk bytes), and null means unknown. Check details are bounded sanitized messages. Values contain only a marker-reported account, version, absolute path or nonnegative free byte count. Older health objects remain valid with null checks and an empty executable map. Overall healthy still means fresh compatible exact-tree service, not an aggregate of optional tool/disk checks. Missing Git does not hide PowerShell, account or disk facts. A stale marker can retain diagnostic facts, but does not become healthy because a path is present. Paths are not executed or re-probed by health; the current worker does not measure PowerShell version, so its version value is null/unknown.

The total connection/retry/SFTP health deadline is SSH_CONNECT_TIMEOUT + 5 seconds (15 by default); the existing per-alias/spool cache lasts at most five seconds. Timeout/access/schema failure returns structured unhealthy diagnostics, with ssh=true only if a connection succeeded. External caller cancellation is not turned into a successful check. The nested versioned facts include native platform/account, instance ID, UTC heartbeat, exact-tree/stdin/PTY flags, free disk bytes, absolute PowerShell/Git/Node/.NET/Codex paths (null when absent), and PowerShell version when known. No remote commands are needed; no environment or credentials are returned. Missing/stale markers return structured unhealthy results even under runner=required. For example, call runner_health({"server_name":"windows-build"}) through the MCP tool form.

The ten questions in evaluations/ssh_remote_runner_read_only.xml use a fixed local fixture and only health/status/log/preview/artifact metadata calls. tests/test_runner_health.py verifies all answers through registered read-only tools. Its alias named production is synthetic; never run this fixture against actual production infrastructure. It is not live service, transfer or connector acceptance.

The service loop calls await worker.publish_capabilities() regularly; the method throttles publication to ten seconds and uses the existing independent disk-I/O boundary. Initial publication requires spool/policy validation and a fixed contained child self-check; it never runs caller code. Startup enforces private spool ownership/permissions. A noninherited native exclusive lock is held through worker shutdown. On next acquisition only the previous valid 32-hex owner is recovered, before new heartbeat/scan; its incomplete jobs become interrupted and are never reexecuted. Other owners and ownerless claims remain untouched. Do not remove/replace service-owner.lock to bypass duplicate-host errors. Keep it and the entire spool together across upgrades and restarts. If recovery encounters a publication or metadata error, startup preserves the previous owner record and refuses new heartbeat/scan. Repair the reported spool permissions/metadata, then restart; completed recoveries are retained and the remaining jobs can be recovered without replay. Graceful service stop can preserve a known native process exit code in interrupted state/result; hard-crash recovery has a null exit when no native outcome was recorded. Both paths retain their durable outcome consistently.

Each child starts suspended, joins a private kill-on-close Job Object, then resumes with only its stdin/stdout/stderr handles inherited. Waiting includes descendants after the leader exits.

Linux requires an explicitly supplied writable delegated cgroup v2 root with cgroup.kill support; it fails capability checks without that delegation. A gate delays user execution and request environment application until cgroup assignment. This contains ordinary descendants, including setsid, but is not a sandbox against deliberate cgroup migration or external privileged launchers. Native Linux acceptance can run with NPROX_TEST_CGROUP_ROOT set to an existing disposable delegated root; tests do not create delegation or change systemd policy. Local Windows tests passed; native Linux containment acceptance remains pending on a Python-equipped Linux test host.

Runner PowerShell/pwsh scripts can call the codex alias when executables.codex contains a validated absolute executable path. All shells receive that configured path as NPROX_CODEX_EXECUTABLE: cmd can use call "%NPROX_CODEX_EXECUTABLE%", and sh/bash can use "$NPROX_CODEX_EXECUTABLE". The configured value takes precedence over request overrides. Other shells have no implicit alias; their ordinary jobs remain supported, and no Codex directory or shim is added to PATH.

Cancelling an stdin-write caller does not retract bytes already being written. The managed process retains and serializes that native operation; closing stdin waits for it without blocking the event loop. A timeout/shutdown caller must cancel the managed process to release a nonreading child's pipe. Each managed process lazily owns one stdin worker, isolated from other jobs and the shared executor. Native tree cancellation does not use that worker. Waiting for completion drains/closes stdin and waits asynchronously for its worker to exit, so blocked input in another job cannot delay publication.

Runner installation and rollback

These are operator-run instructions, not evidence of an installation. Install the pinned reviewed ssh-mcp-vn==0.6.3 release into a persistent environment. Both ssh-mcp-vn (MCP client-side server) and ssh-mcp-runner (remote local-spool worker) are included. The runner opens no network listener and needs no SSH credentials. Never run a service from an ephemeral uvx cache.

Use a dedicated least-privilege service identity, a local filesystem with native file-lock support, and existing absolute spool/cwd/executable paths. Reject symlinks, reparse points and shared/network spools. The spool must be owned by the service identity; only that identity, SYSTEM and administrators may mutate a Windows spool or inherit write access to its children. Linux rejects group/other write bits; use mode 0700 and umask 0077. Align SSH/SFTP writer identity with that policy. A separate ordinary SSH account granted spool write access is incompatible with this strict policy; do not loosen it to make startup pass. Administrators can still change the spool: this is trusted-job containment, not a hostile multi-tenant sandbox. Keep service control rights separate from ordinary job submission.

Spool requests/scripts/env/logs are durable and may contain sensitive job data. Provision disk quota, monitor disk_free_bytes, and apply reviewed offline retention; automatic rotation/cleanup is absent. Back up the complete spool and owner record only after stopping the service. Do not erase identity records, replay ambiguous jobs with new keys, or restore an old snapshot over an active spool.

Windows SCM

Provision a persistent Python 3.12+ environment with the reviewed wheel and runner-windows extra. Choose a built-in/passwordless managed service account; the CLI never accepts a password. Its default is NT AUTHORITY\LocalService, not LocalSystem. A regular password-requiring account needs a separately approved credential-managed installation workflow; do not place its password in command lines.

For a fresh host, run the bundled installer from an elevated PowerShell. It creates the fixed venv, private spool/work directories, stable Codex executable, auto-start service, and verifies the live capability marker. Pass explicit executable paths when auto-detection is unsuitable:

.\deploy\install-windows-runner.ps1 -PythonExe 'C:\Python312\python.exe' -CodexExe 'C:\Tools\Codex\codex.exe'
py -3.12 -m venv 'C:\Program Files\nProxRunner\venv'
& 'C:\Program Files\nProxRunner\venv\Scripts\python.exe' -m pip install 'ssh-mcp-vn[runner-windows]==0.6.3'

Create the spool and grant only the chosen service SID plus SYSTEM/Administrators full control, remove inherited broad grants, and set the owner to the service SID. Example for a new empty dedicated directory with the default LocalService SID (administrator shell; review paths before applying):

New-Item -ItemType Directory -Path 'C:\nprox\jobs'
icacls 'C:\nprox\jobs' /inheritance:r
icacls 'C:\nprox\jobs' /grant:r '*S-1-5-19:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F'
icacls 'C:\nprox\jobs' /setowner '*S-1-5-19'
& 'C:\Program Files\nProxRunner\venv\Scripts\ssh-mcp-runner.exe' install-windows-service --spool 'C:\nprox\jobs' --cwd-root 'C:\build workspace' --executable 'powershell=C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' --executable 'codex=C:\Tools\Codex\codex.exe'
Start-Service -Name NProxSshRunner
Get-Service -Name NProxSshRunner

Provision cwd permissions separately for the same identity. Installation registers a fixed auto-start service but does not start it; the registered command uses the absolute environment Python with -I -m ssh_mcp_windows_service. Optional pywin32 imports remain isolated from normal Linux usage. Verify runner_health through the configured alias and confirm account, paths and fresh heartbeat. Windows SCM recovery actions are not configured by this package: configure approved restart-on-failure policy separately if desired. Graceful stop/shutdown drains jobs; a crash closes Job Objects, then the next service start recovers the previous owner's incomplete jobs as interrupted.

Uninstall: stop NProxSshRunner, wait for Stopped, then run the same absolute runner executable with remove-windows-service. Removal refuses a running service and retains all spool/job data. To roll back, stop/remove registration, restore the previously reviewed environment and matching policy, register/start again, and verify health before enabling submissions. Preserve spool/alias/idempotency keys; never roll an active job back to an agentless launcher. Review protocol compatibility before downgrading; use runner=required to fail closed during maintenance.

Linux systemd

Provision a dedicated nprox-runner account/group and persistent environment such as /opt/nprox-ssh-runner/venv. Install the reviewed wheel there. The wheel contains share/ssh-mcp-vn/ssh-mcp-runner.service under its installation prefix; copying it to systemd is an explicit operator step. Review it first, then adapt ExecStart to the absolute venv executable:

python3.12 -m venv /opt/nprox-ssh-runner/venv
/opt/nprox-ssh-runner/venv/bin/python -m pip install ssh-mcp-vn==0.6.3
ExecStart=/opt/nprox-ssh-runner/venv/bin/ssh-mcp-runner run --spool /var/lib/nprox-ssh-runner/jobs --cwd-root /var/lib/nprox-ssh-runner --cgroup-root /sys/fs/cgroup/system.slice/ssh-mcp-runner.service --executable codex=/opt/codex/bin/codex

Keep User=nprox-runner, Group=nprox-runner, UMask=0077, StateDirectoryMode=0700, StateDirectory=nprox-ssh-runner/jobs, Delegate=yes, KillMode=mixed and the bounded stop timeout. The host must use cgroup v2 with writable delegated cgroup.procs and cgroup.kill; systemd must own the matching cgroup path. Change --cgroup-root if the service unit/slice name changes. Process groups, setsid, or a regular directory pretending to be a cgroup are not acceptable replacements. The packaged unit creates a private state directory; provision its permitted work directories and the SSH writer identity consistently. Do not grant jobs general sudo/systemd control.

sudo systemctl daemon-reload
sudo systemctl enable --now ssh-mcp-runner.service
sudo systemctl status ssh-mcp-runner.service

After installation verify healthy SSH/SFTP discovery and a small explicit test job. On stop the worker drains its processes before systemd's final subtree cleanup; on crash/reboot the service manager cleans the old service cgroup and a new lock owner recovers interrupted jobs without reexecution. Running a worker manually outside that service manager does not prove orphaned Linux descendants were cleaned up. No recovery guesses owners from heartbeat age or touches ownerless/foreign claims.

Uninstall: sudo systemctl disable --now ssh-mcp-runner.service, verify it stopped, remove only the reviewed unit file from /etc/systemd/system, and run daemon-reload. Keep the spool and environment until the retention/rollback decision. Rollback restores the prior reviewed environment/unit, reloads, starts, and verifies health with the same spool and alias. Stop before replacing binaries or policy.

Clean tool environment

Configure absolute --executable tool=path on the runner and corresponding absolute executables in the MCP alias policy. codex must point directly to the intended executable, not rely on a shell shim or app-injected PATH. Never prepend OpenAI/Codex/bin to PATH: it can shadow tools such as git. PowerShell gets the configured codex alias; other shells use NPROX_CODEX_EXECUTABLE as described above. Health reports null for missing tools rather than inventing paths. The child environment is minimal; approve explicit request env values separately and never treat health as an env dump.

Guarded disposable service acceptance

As of 2026-09-15 no SSH_RUNNER_TEST_CONFIG was supplied: live Windows SCM/Linux systemd restart, hard-crash/reboot, native Linux containment and ChatGPT connector acceptance are unverified. Local subprocess lock/crash and worker tests are not remote deployment proof.

Only an operator who has already installed isolated disposable services should run the harness. It creates jobs/counter files and stops, starts, restarts and forcibly terminates the fixed service host; it never installs services. Use a private absolute JSON file (maximum 64 KiB), no inline passwords, tokens, environment or arbitrary commands. Authentication uses your approved SSH agent or key path. The strict shape is shown below; targets may contain linux, windows, or both. Windows uses alias disposable-runner-windows, platform windows, and paths such as C:/ssh-runner-acceptance/jobs and C:/ssh-runner-acceptance/work. Each cwd must be the sole permitted root; spool/work must be separate absolute paths with the literal ssh-runner-acceptance component. Service policy must match.

{
  "version": 1,
  "disposable": true,
  "purpose": "ssh-runner-acceptance",
  "allow_service_restart": true,
  "targets": {
    "linux": {
      "server": {
        "name": "disposable-runner-linux", "host": "disposable.example.invalid",
        "user": "runner-test", "platform": "linux", "runner": "auto",
        "job_spool": "/srv/ssh-runner-acceptance/jobs",
        "allowed_cwd_roots": ["/srv/ssh-runner-acceptance/work"]
      },
      "cwd": "/srv/ssh-runner-acceptance/work"
    }
  }
}

The marker/name guard prevents accidental ordinary configs, not a malicious operator or DNS change. Verify the isolated host out of band; the legacy SSH connector still disables host-key validation. The test identity needs narrowly approved control of NProxSshRunner or passwordless sudo for only the fixed /usr/bin/systemctl operations on ssh-mcp-runner.service (including the explicit main process kill test). These privileges are for disposable acceptance, not normal job accounts.

$env:SSH_RUNNER_TEST_CONFIG='C:\private-tests\runner-disposable.json'
$env:SSH_RUNNER_TEST_ALLOW_RESTART='disposable-services-only'
uv run pytest tests/integration/test_runner_service.py -v --tb=no --show-capture=no

Without the config the whole module skips during collection, before server import or SSH. With an invalid/ambiguous config or missing second acknowledgement collection refuses safely. Never use --showlocals, verbose SSH logging or captured config dumps for live tests. Summaries must omit host, credential, account/path and job payload details; retain detailed evidence privately. Tests verify fresh health/tool paths, native exit/log reconnect, service preference, safe unavailable-service fallback, graceful interruption with consistent native exit/state/result, hard-crash interruption with null exit, no double execution, and a new job after restart. A real host reboot remains a separate operator-controlled acceptance step, not performed by this harness. Retain the test's tiny counters and durable jobs for inspection; no blanket remote cleanup is run. The existing scripts/smoke_test.py expects a standard server config, not this guarded wrapper; do not pass it this file or any unknown production configuration.

Metadata

Release files for ssh-mcp-vn 0.6.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ssh-mcp-vn 0.6.3
File Size Uploaded Provenance
ssh_mcp_vn-0.6.3.tar.gz 257.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for ssh-mcp-vn 0.6.3
File Interpreter ABI Platform
ssh_mcp_vn-0.6.3-py3-none-any.whl Python 3 none any Details

Total release size: 364.9 kB

Release files / ssh_mcp_vn-0.6.3.tar.gz

Download URL ssh_mcp_vn-0.6.3.tar.gz
Size 257.3 kB
Tags Source
SHA-256 checksum
How to use checksums
1eefc77113dde0bcb86070cc9d800ea1e657b5b87037142006f6aea81fe0d210
BLAKE2b-256 checksum
How to use checksums
28bedab6ce8c3a76591c2a90e5c82ed91c07badb088acc6628bc33fb92bb5eeb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / ssh_mcp_vn-0.6.3-py3-none-any.whl

Download URL ssh_mcp_vn-0.6.3-py3-none-any.whl
Size 107.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b940f8ba6fc40576048a6cfa9d306f619c6f70a940af4c59c5379337de3d8e62
BLAKE2b-256 checksum
How to use checksums
f01d2f22e4b86cefd09c0c2a60f1f859a6f6f64962f9ed8abc92be56d497217c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for ssh-mcp-vn 0.6.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page