Metadata-Version: 2.4
Name: ssh-mcp-vn
Version: 0.5.2
Summary: High-performance SSH & SFTP MCP Server for Linux
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)

Một MCP (Model Context Protocol) Server mạnh mẽ dùng để thực thi lệnh (SSH) và truyền tải file (SFTP) trên **nhiều máy chủ Linux từ xa (multi-host)** cùng lúc.

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

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

- **Hỗ trợ Multi-host**: Có thể tương tác với nhiều máy chủ Linux mà không cần chạy nhiều instance.
- **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.

```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"
      ],
      "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",
        "--url-host",
        "https://nprox.dev/api/external/devops/servers"
      ],
      "env": {
        "SSH_URL_HOST_TOKEN": "<NPROX_API_TOKEN>"
      }
    }
  }
}
```

Endpoint phải trả về object có mảng `servers`; mỗi item cần `name`, `host`, `port` và
`username`, có thể kèm `description`. Server map `username` thành SSH `user`; `description`
thiếu được hiển thị là chuỗi rỗng. API không trả private key/password. Nếu một alias cần mật khẩu
hoặc key riêng, đặt `SSH_CREDENTIALS` trong secret environment của Hub; chỉ `name`, `password` và
`key_path` được chấp nhận. Host, username và port luôn giữ theo inventory.

```json
{
  "SSH_CREDENTIALS": "[{\"name\":\"Personal Server\",\"password\":\"<SSH_PASSWORD>\"}]"
}
```

Không đặt giá trị secret này trong `args`, log hay repository. Alias trong `SSH_CREDENTIALS` phải
đúng một alias inventory trả về.

### 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"
      ],
      "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"
      ],
      "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 --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.
- `description` (tùy chọn): Mô tả ngắn hiển thị trong `list_ssh_servers`.
- `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 --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 --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.

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

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

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

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

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

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

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

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

### macOS

```bash
sftp -i "$HOME/.ssh/id_ed25519" mcp-upload@server-b
```

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

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

```text
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)

- `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ê mọi server đã cấu hình với `name`, `description`, `host` và `ip`.

> `list_ssh_servers` luôn xuất hiện. Khi chỉ cấu hình **1 server**, `server_name` của các tool là
> tùy chọn; khi có nhiều server, phải truyền alias được tool list trả về.

### Chọn server theo alias

Mọi SSH/SFTP tool chỉ dùng `server_name`; host, user, port và credential được lấy từ inventory
đã cấu hình hoặc SSH agent/key mặc định của MCP Hub. Tool không nhận credential trong lời gọi.

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

1. Có `server_name` → dùng server đã cấu hình.
2. Không có `server_name` → fallback về server duy nhất đã cấu hình.
3. Không có server, hoặc có nhiều server mà không truyền alias → báo lỗi có danh sách alias.

```text
ssh_execute_command
  command: df -h
  server_name: production
```

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

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

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

```text
ssh_execute_command
  command: df -h
```

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

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

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

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

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

**4. Xem danh sách server đã cấu hình**

```text
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.5.2 - 2026-09-01

**Fixed**
- `SSH_CREDENTIALS` có thể bổ sung password/key path theo alias sau khi tải inventory; không thể ghi đè host, user hoặc port từ inventory.

### 0.5.1 - 2026-09-01

**Fixed**
- Remote inventory now sends an explicit User-Agent so endpoints that reject Python's default
  client still return configured SSH servers.

### 0.5.0 - 2026-09-01

**Breaking**
- Mọi tool SSH/SFTP chỉ nhận `server_name`; xoá kết nối inline qua `host`, `user`, `port`,
  `password`, `key_path`.

**Changed**
- `list_ssh_servers` luôn có mặt và trả `name`, `description`, `host`, `ip` mà không lộ thông tin xác thực.

### 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](CHANGELOG.md).

## License
MIT
