Metadata-Version: 2.4
Name: ssh-mcp-vn
Version: 0.7.1
Summary: SSH and SFTP MCP server: remote commands, directory listing and verified file transfers
Author-email: NamHT <namht.dev@gmail.com>
License-Expression: MIT
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: mcp<2,>=1.2.0
Requires-Dist: asyncssh>=2.14.0
Requires-Dist: pydantic>=2.8.0
Requires-Dist: pydantic-settings>=2.4.0
Requires-Dist: python-dotenv>=1.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
Requires-Dist: ruff>=0.5.0; extra == "dev"
Requires-Dist: mypy>=1.10.0; extra == "dev"

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

MCP server SSH/SFTP cho nhiều máy chủ Windows/Linux. Từ `0.7.0` server chỉ gồm sáu tool:
chạy lệnh, tạo thư mục, liệt kê thư mục, upload/download file có xác minh SHA-256 và liệt kê
server đã cấu hình. Durable job, nProx Runner, PTY và các tool SFTP legacy đã bị gỡ; xem
[CHANGELOG](CHANGELOG.md) để biết cách chuyển đổi.

## Verified file transfers

`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. 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/backups"]` or
`["C:/releases", "C:/backups"]`. 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.

`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:

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

Call `sftp_download` with the same shape to fetch a file into staging; create the destination
folder (for example `logs`) inside staging first. File bytes never enter MCP JSON.

## 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:

```bash
uvx ssh-mcp-vn==0.7.1
```

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

- **Multi-host**: tương tác với nhiều máy chủ mà không cần chạy nhiều instance.
- **Thực thi qua SSH**: chạy lệnh shell trên máy chủ từ xa.
- **Quản lý thư mục**: tạo và liệt kê thư mục từ xa.
- **Truyền file có xác minh**: SFTP upload/download với SHA-256, giới hạn root và ghi nguyên tử.

## 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.

```json
[
  {
    "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 đó:

```json
{
  "mcpServers": {
    "ssh-server": {
      "command": "uvx",
      "args": [
        "ssh-mcp-vn==0.7.1"
      ],
      "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.

```json
{
  "mcpServers": {
    "ssh-server": {
      "command": "uvx",
      "args": [
        "ssh-mcp-vn==0.7.1",
        "--url-host",
        "https://nprox.dev/api/external/devops/servers"
      ],
      "env": {
        "SSH_URL_HOST_TOKEN": "<NPROX_API_TOKEN>",
        "SSH_REMOTE_POLICY": "{\"windows-build\":{\"allowed_transfer_roots\":[\"C:/releases\"]}}"
      }
    }
  }
}
```

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ả.

Inventory từ API được nạp lại tự động khi cache quá `SSH_URL_HOST_TTL` giây (mặc định 60; `0` là
nạp lại mỗi lần dùng tool), nên server thêm, xóa hoặc đổi credential ở API sẽ có hiệu lực mà không
cần restart. Gọi `list_ssh_servers` với `refresh=true` để nạp lại ngay. Nếu API lỗi lúc nạp lại,
server giữ inventory cũ và ghi cảnh báo ra stderr. Với file hoặc env, cache chỉ đổi khi gọi `refresh=true`.

`SSH_REMOTE_POLICY` là object JSON theo tên server, chỉ cho phép field không chứa secret
`allowed_transfer_roots`. Nó không thể thay `host`, `user`, port hoặc credential.

### 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):**

```json
{
  "mcpServers": {
    "ssh-server": {
      "command": "uvx",
      "args": [
        "ssh-mcp-vn==0.7.1"
      ],
      "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):**

```json
{
  "mcpServers": {
    "ssh-server": {
      "command": "uvx",
      "args": [
        "ssh-mcp-vn==0.7.1"
      ],
      "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):

```bash
uvx ssh-mcp-vn==0.7.1 --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:

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

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

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

Smoke test dùng lệnh:

```bash
hostname && whoami && pwd && uptime
```

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

```bash
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.

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

| Tool | Mục đích |
| --- | --- |
| `ssh_execute_command` | Chạy một lệnh shell và chờ kết thúc (giới hạn `SSH_COMMAND_TIMEOUT`, mặc định 30 giây). |
| `ssh_create_folder` | Tạo thư mục từ xa, tạo cả thư mục cha (`mkdir -p`). Cần trước khi upload vào thư mục mới. |
| `sftp_list_directory` | Liệt kê tên và kích thước file trong một thư mục từ xa. |
| `sftp_upload` | Upload file từ staging root của MCP host, xác minh SHA-256 và ghi nguyên tử. |
| `sftp_download` | Download file từ xa vào staging root của MCP host, xác minh SHA-256 và ghi nguyên tử. |
| `list_ssh_servers` | Liệt kê server đã cấu hình (không trả password). Luôn có mặt; `refresh=true` nạp lại inventory ngay, ví dụ sau khi thêm/xóa server ở API. |

> Khi chỉ cấu hình **1 server**, `server_name` là **tùy chọn**. Khi có **nhiều server**, `server_name`
> là bắt buộc; gọi `list_ssh_servers` để xem alias hợp lệ.

Mọi tool chỉ nhận configured alias qua `server_name`; không tool nào nhận `host`, `user`, `port`,
`password` hay `key_path` trong lời gọi. Credential chỉ nằm trong cấu hình server.

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

```text
ssh_execute_command
  server_name: toppion-linux
  command: df -h
```

```text
ssh_create_folder
  server_name: toppion-linux
  path: /srv/releases/2026-09
```

```text
sftp_list_directory
  server_name: toppion-linux
  path: /srv/releases
```

```json
{"local_path":"release/app.zip","remote_path":"/srv/releases/2026-09/app.zip","server_name":"toppion-linux"}
```

Gọi `sftp_upload` với object trên; `sftp_download` dùng cùng shape. Mọi input đều có mô tả,
ràng buộc, giá trị mặc định và ví dụ trong JSON Schema (được `tests/test_tool_schema.py` kiểm tra).

## Biến môi trường vận hành

| Biến | Mặc định | Ý nghĩa |
| --- | --- | --- |
| `SSH_COMMAND_TIMEOUT` | `30` | Số giây tối đa `ssh_execute_command` chờ một lệnh kết thúc. Lệnh chạy lâu hơn cần được tách nhỏ hoặc chạy nền bằng `nohup` rồi đọc log. |
| `SFTP_STAGING_ROOT` | không có | Thư mục tuyệt đối, đã tồn tại, trên máy chạy MCP; nơi `sftp_upload`/`sftp_download` đọc và ghi file. Bắt buộc để dùng truyền file. |
| `SFTP_MAX_FILE_SIZE` | `2147483648` | Giới hạn kích thước file (byte, số nguyên dương) cho mỗi lần truyền. |
| `SSH_URL_HOST_TTL` | `60` | Số giây inventory từ `--url-host` được coi là còn mới (số không âm; `0` = luôn nạp lại). Giá trị sai dùng mặc định. |
| `SSH_URL_HOST_TOKEN` | không có | Bearer token cho `--url-host`; chỉ đặt trong env, không đưa vào `args`. |

## Changelog

### 0.7.1 - 2026-10-21

**New**
- Inventory từ `--url-host` tự nạp lại sau `SSH_URL_HOST_TTL` giây (mặc định 60); lỗi API lúc nạp
  lại giữ inventory cũ.
- `list_ssh_servers` luôn được đăng ký và nhận `refresh` (boolean, mặc định false) để nạp lại ngay.

### 0.7.0 - 2026-10-21

**Breaking**
- Thu gọn server còn sáu tool: `ssh_execute_command`, `ssh_create_folder`, `sftp_list_directory`,
  `sftp_upload`, `sftp_download`, `list_ssh_servers` (chỉ khi cấu hình nhiều server).
- Gỡ durable job, nProx Runner, PTY, Windows installer và các tool `sftp_upload_file`,
  `sftp_download_file`, `sftp_upload_staged_file`. Việc chạy lâu phải đi qua `ssh_execute_command`.
- Gỡ inline connection (`host`, `user`, `port`, `password`, `key_path`) khỏi mọi tool; chỉ dùng alias
  trong `server_name`.
- `SSH_REMOTE_POLICY` chỉ còn nhận `allowed_transfer_roots`.

**Fixed**
- `sftp_list_directory` không còn trả ký tự xuống dòng dạng literal giữa các mục.

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

## License
MIT

