mcp-server-sftp
by wpfyorg
README.md
# mcp-server-sftp
A fast [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that exposes an **SFTP** connection as tools — read, write, delete, list, mkdir, rmdir, rename, upload, download, stat, exists — plus **pipelined batch tools** (`write_many`, `delete_many`, `upload_many`, `download_many`) and a `sync_dir` for folder deploys.
It speaks SFTP over raw `ssh2`, so it works even against **chrooted, SFTP-only accounts** (`ForceCommand internal-sftp`, i.e. hosts with no shell exec). An optional write-path guard restricts mutating operations to a subtree.
## Features
- **Full file ops over SFTP** — including `delete` / `rmdir`, which many SSH MCP servers omit.
- **Batch + concurrent** — `*_many` tools and `sync_dir` issue many requests in flight on a single channel, so bulk operations collapse from N × round-trips to ~1 × round-trip. On a high-latency link this is an order of magnitude faster (see [Performance](#performance)).
- **Works on shell-less hosts** — no `exec` required; pure SFTP subsystem.
- **Write guard** — `SFTP_ALLOWED_PREFIX` refuses writes/deletes/renames outside a chosen subtree (reads stay unrestricted). Applied per item in batch tools.
- **Warm, reused connection** — one `ssh2` client + SFTP channel is opened at startup and reused, with automatic reconnect on drop, so the first tool call doesn't pay the SSH handshake.
- **Credentials via env only** — nothing is hard-coded; supports password or private-key auth.
## Requirements
- Node.js >= 18
- An SSH/SFTP account on the target host
## Installation
```bash
git clone https://github.com/wpfyorg/mcp-server-sftp.git
cd mcp-server-sftp
npm install
```
## Configuration
The server is configured entirely through environment variables:
| Variable | Required | Default | Description |
|---|---|---|---|
| `SFTP_HOST` | yes | — | Hostname or IP of the SFTP server |
| `SFTP_USER` | yes | — | SSH/SFTP username |
| `SFTP_PASSWORD` | * | — | Password auth (required unless using a key) |
| `SFTP_PORT` | no | `22` | SSH port |
| `SFTP_PRIVATE_KEY` | * | — | Path to a private key file (alternative to password) |
| `SFTP_PASSPHRASE` | no | — | Passphrase for the private key, if any |
| `SFTP_ALLOWED_PREFIX` | no | _(none)_ | If set, writes/deletes/renames must be under this absolute path |
| `SFTP_MAX_READ_BYTES` | no | `1048576` | `sftp_read` refuses files larger than this (use `sftp_download`) |
| `SFTP_READY_TIMEOUT` | no | `20000` | SSH handshake timeout (ms) |
| `SFTP_MAX_CONCURRENCY` | no | `8` | Max in-flight operations per batch/`sync_dir` call |
| `SFTP_TRANSFER_CONCURRENCY` | no | `64` | Parallel chunks for `upload`/`download` (`fastPut`/`fastGet`) |
| `SFTP_CHUNK_SIZE` | no | `32768` | Chunk size in bytes for `fastPut`/`fastGet` |
\* Provide at least one of `SFTP_PASSWORD` or `SFTP_PRIVATE_KEY`.
### MCP client configuration
Add the server to your MCP client config (e.g. Cursor `.cursor/mcp.json`, Claude Desktop, etc.):
```json
{
"mcpServers": {
"sftp": {
"command": "node",
"args": ["/absolute/path/to/mcp-server-sftp/index.mjs"],
"env": {
"SFTP_HOST": "203.0.113.10",
"SFTP_PORT": "22",
"SFTP_USER": "deploy",
"SFTP_PASSWORD": "••••••••",
"SFTP_ALLOWED_PREFIX": "/var/www/html/wp-content"
}
}
}
}
```
## Tools
| Tool | Arguments | Notes |
|---|---|---|
| `sftp_list` | `remotePath` | List a directory (read-only) |
| `sftp_read` | `remotePath` | Read a text file; refuses files > `SFTP_MAX_READ_BYTES` |
| `sftp_write` | `remotePath`, `content`, `createDirs?` | Write/overwrite a text file |
| `sftp_delete` | `remotePath` | Delete a file |
| `sftp_mkdir` | `remotePath`, `recursive?` (default `true`) | Create a directory |
| `sftp_rmdir` | `remotePath`, `recursive?` (default `false`) | Remove a directory |
| `sftp_rename` | `fromPath`, `toPath` | Rename/move (destination is guarded) |
| `sftp_upload` | `localPath`, `remotePath`, `createDirs?` | Upload a local file (binary-safe) |
| `sftp_download` | `remotePath`, `localPath` | Download to a local path |
| `sftp_stat` | `remotePath` | Stat a path (read-only) |
| `sftp_exists` | `remotePath` | Returns `false` \| `'d'` \| `'-'` \| `'l'` |
| `sftp_write_many` | `files:[{remotePath,content}]`, `createDirs?`, `concurrency?` | Write many text files in parallel |
| `sftp_delete_many` | `remotePaths:[...]`, `concurrency?` | Delete many files in parallel |
| `sftp_upload_many` | `files:[{localPath,remotePath}]`, `createDirs?`, `concurrency?` | Upload many local files in parallel |
| `sftp_download_many` | `files:[{remotePath,localPath}]`, `concurrency?` | Download many files in parallel |
| `sftp_sync_dir` | `localDir`, `remoteDir`, `delete?`, `dryRun?`, `concurrency?` | Sync a local folder to remote (upload missing/changed/newer; optional delete of orphans) |
All paths must be **absolute**. Mutating tools (`write`, `delete`, `mkdir`, `rmdir`, `rename`, `upload`, all `*_many`, `sync_dir`) are subject to `SFTP_ALLOWED_PREFIX` when set; reads are always allowed. Batch tools return a per-item PASS/FAIL summary and report `isError` if any item failed.
## Performance
The physical floor on a remote host is **network round-trip time (RTT)**, and every SFTP operation costs several sequential round-trips. Against a host ~311 ms away, issuing operations **one at a time** is what makes a "deploy" feel slow — not the byte count.
SFTP allows many requests in flight on a single channel, so the batch tools and `sync_dir` **pipeline** their work over the one warm connection. Measured on the ~311 ms host (10 tiny files, raw `ssh2`):
| Workload | Sequential | Pipelined (batch tool) | Speed-up |
|---|---|---|---|
| write × 10 | ~25.6 s | **~2.2 s** | ~11× |
| stat × 10 | ~8.9 s | **~0.9 s** | ~10× |
| delete × 10 | ~6 s | **~2.0 s** | ~3× |
Rule of thumb: **N files done sequentially ≈ N × RTT; done in one batch ≈ 1 × RTT.** For a real folder deploy, prefer `sftp_sync_dir` (or a `*_many` tool) over a loop of single calls.
Notes:
- The warm connection means the one-time SSH handshake is paid at startup, not on your first tool call.
- Beyond pipelining, the only way past the RTT floor is to run the server **near the host** (same region/datacenter, RTT → ~1 ms) — an infrastructure change, out of scope for this server.
## Security notes
- Credentials are read from the environment — keep them in your MCP client config or a secrets manager, not in source.
- `SFTP_ALLOWED_PREFIX` is defense-in-depth on top of the server's own filesystem permissions; set it to the narrowest writable subtree you need.
- The server never invokes a shell; it uses the SFTP subsystem only.
## License
MIT
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues