sftp-mcp
Provides a browsable, mutable SFTP filesystem for AI agents, supporting file operations like listing, uploading, downloading, moving, and deleting.
README
sftp-mcp
SFTP as a browsable, mutable filesystem for AI agents — list, stat, lstat, realpath, move, mkdir, delete, batch, symlink, tree upload, disk usage. Not just upload/download. Node runtime, served over both stdio and streamable HTTP, zero required native deps. Ships with a read-only mode and path jail so it's safe to expose over a shared endpoint.
Most SFTP/SSH MCP servers are SSH-first: their file story is upload/download of individual files bolted onto remote command execution.
sftp-mcptreats the remote as a filesystem — the operations that make it browsable and mutable.
- Quick start
- Tool reference — connection params · the 17 tools
- Credentials & security
- Hardening — read-only mode & path jail
- Responses, limits & errors
- Transports · Docker · Development
Quick start
npx @cordfuse/sftp-mcp # stdio (default)
npx @cordfuse/sftp-mcp --http # streamable HTTP on :3901 (PORT to change)
MCP client config (stdio — Claude Code, Cursor, local agents):
{
"mcpServers": {
"sftp": { "command": "npx", "args": ["-y", "@cordfuse/sftp-mcp"] }
}
}
Once wired, ask your agent to "list /var/www on sftp.example.com as user
deploy" and it will call list_files with the connection + path.
Tool reference
Seventeen tools. Every tool is self-contained: each call carries its own connection details (there is no server-side config — see Credentials), so a single running server can address any number of SFTP hosts, switched call-to-call.
Connection parameters (every tool)
These fields are accepted by all tools. They are omitted from the per-tool tables below to avoid repetition.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
host |
string | yes | — | SFTP server hostname or IP. |
username |
string | yes | — | SSH username. |
port |
number | no | 22 |
TCP port. |
password |
string | no | — | Inline password. |
privateKey |
string | no | — | Inline private key (PEM / OpenSSH text). |
passphrase |
string | no | — | Passphrase for an encrypted privateKey. |
timeoutMs |
number | no | 15000 |
Connection (ready) timeout in ms. |
Secret resolution order: password → privateKey (+ passphrase) → if
neither is given, the local ssh-agent / default ~/.ssh key (stdio only).
See Credentials.
Authenticating a call. Every tool takes exactly one of these three forms.
The same auth fields apply to every tool (shown here on test_connection):
// 1. Private key — preferred
{
"host": "sftp.example.com", "username": "deploy",
"privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\n…\n-----END OPENSSH PRIVATE KEY-----",
"passphrase": "only-if-the-key-is-encrypted"
}
// 2. Password
{ "host": "sftp.example.com", "username": "deploy", "password": "s3cr3t" }
// 3. No secret → local ssh-agent / ~/.ssh key (stdio only; keeps keys out of the model)
{ "host": "sftp.example.com", "username": "deploy" }
The per-tool examples below omit the secret for brevity — they assume form 3 (local key). For explicit auth, add
privateKey(form 1, preferred) orpassword(form 2) to any of them.
FileType (used below) is one of: "file", "directory", "symlink",
"other".
The tools
test_connection
Connect and immediately disconnect — validates credentials and reachability.
No parameters beyond the connection fields.
Returns: { ok: true, message: string }
{ "host": "sftp.example.com", "username": "deploy" }
list_files
List a directory, with optional glob/type filtering, sorting, and recursion.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | yes | — | Remote directory to list. |
wildcard |
string | no | — | Glob on the entry name, e.g. *.pdf (* and ?, case-insensitive). |
types |
FileType[] |
no | all | Keep only these entry types. |
sortField |
"name" | "size" | "modifyTime" |
no | — | Field to sort by. |
sortDirection |
"asc" | "desc" |
no | asc |
Sort direction. |
recursive |
boolean | no | false |
Descend into subdirectories. |
limit |
number | no | — | Cap the number of entries returned. |
Returns: FileEntry[] where FileEntry = { name, path, type: FileType, size: number, modifyTime: number } (modifyTime is epoch ms).
{
"host": "sftp.example.com", "username": "deploy",
"path": "/var/www/releases",
"wildcard": "*.tar.gz", "types": ["file"],
"sortField": "modifyTime", "sortDirection": "desc"
}
stat
Metadata for a single path. Doubles as an existence check.
| Param | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Remote path to stat. |
Returns: { exists: boolean, type?: FileType, size?: number, modifyTime?: number, accessTime?: number, mode?: string } — mode is octal permission bits as a string (e.g. "644"). When exists is false, no other fields are present.
{ "host": "sftp.example.com", "username": "deploy", "path": "/var/www/app.tar.gz" }
lstat
Like stat, but does not follow a final symlink — reports the link itself
(type: "symlink") rather than its target. Use it to detect symlinks; use stat
to see what a link resolves to.
| Param | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Remote path to lstat. |
Returns: same shape as stat. For a symlink, type is "symlink" and
size is the length of the target path string.
realpath
Canonicalize a path — resolve ./.. segments and symlinks to an absolute path.
| Param | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Remote path to resolve. |
Returns: { path: string, realpath: string }
{ "host": "sftp.example.com", "username": "deploy", "path": "/var/www/../www/current" }
read_symlink
Read the target a symbolic link points to (does not follow further).
| Param | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Symlink path to read. |
Returns: { path: string, target: string }
disk_usage
Capacity of the filesystem holding a path — check free space before an upload.
Uses the OpenSSH statvfs@openssh.com extension; errors with code ENOSYS if
the server doesn't support it.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | no | "." |
Any path on the target filesystem. |
Returns: { totalBytes: number, freeBytes: number, availableBytes: number }
download_file
Download one file, base64-encoded.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | yes | — | Remote file path. |
maxBytes |
number | no | 33554432 (32 MiB) |
Refuse files larger than this. |
Returns: { path: string, base64: string, size: number }
{ "host": "sftp.example.com", "username": "deploy", "path": "/var/www/config.json" }
download_files
Batch-download every file in a directory matching an optional wildcard.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | yes | — | Remote directory. |
wildcard |
string | no | — | Glob filter, e.g. *.csv. |
recursive |
boolean | no | false |
Include matching files in subdirectories. |
maxTotalBytes |
number | no | 33554432 (32 MiB) |
Cap on the combined size of the selection. |
Returns: { count: number, files: { path: string, base64: string, size: number }[] }. Throws (with the offending path) if the running total exceeds maxTotalBytes — narrow the wildcard or raise the cap.
{
"host": "sftp.example.com", "username": "deploy",
"path": "/exports", "wildcard": "*.csv", "recursive": true
}
upload_file
Upload base64 data to a remote path.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | yes | — | Destination path (including filename). |
base64data |
string | yes | — | File contents, base64-encoded. |
overwrite |
boolean | no | false |
Replace an existing file. Without it, an existing path is an error. |
maxBytes |
number | no | 33554432 (32 MiB) |
Refuse payloads larger than this. |
Returns: { path: string, bytes: number }
{
"host": "sftp.example.com", "username": "deploy",
"path": "/var/www/robots.txt",
"base64data": "VXNlci1hZ2VudDogKgpEaXNhbGxvdzoK",
"overwrite": true
}
upload_dir
Push a whole directory tree from in-memory files under a base directory,
creating the base and any parent directories as needed (mkdir -p). Pairs with
recursive download_files for full tree round-trips.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | yes | — | Remote base directory (created if missing). |
files |
{ path: string, base64: string }[] |
yes | — | Files to write; each path is relative to the base. |
maxTotalBytes |
number | no | 33554432 (32 MiB) |
Cap on the combined size. |
Returns: { count: number, written: { path: string, bytes: number }[] }
{
"host": "sftp.example.com", "username": "deploy",
"path": "/var/www/site",
"files": [
{ "path": "index.html", "base64": "PGgxPmhpPC9oMT4=" },
{ "path": "assets/app.css", "base64": "Ym9keXt9" }
]
}
delete_file
Delete a single file.
| Param | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Remote file to delete. |
Returns: { ok: true, deleted: string }
delete_dir
Remove a directory.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | yes | — | Remote directory to remove. |
recursive |
boolean | no | false |
Remove contents too (rm -rf). Without it, the directory must be empty. |
Returns: { ok: true, removed: string, recursive: boolean }
make_directory
Create a directory.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | yes | — | Remote directory to create. |
recursive |
boolean | no | false |
Create missing parents (mkdir -p); idempotent. |
Returns: { ok: true, created: string }
move
Rename or move a file or directory. Uses SFTP posix-rename — an atomic
overwrite (no delete-then-rename race) when overwrite is set.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | yes | — | Current path. |
newPath |
string | yes | — | Destination path. |
overwrite |
boolean | no | false |
Replace an existing destination. Without it, an existing newPath is an error. |
Returns: { ok: true, from: string, to: string }
{
"host": "sftp.example.com", "username": "deploy",
"path": "/staging/build.zip", "newPath": "/releases/build.zip"
}
chmod
Change a path's permission bits.
| Param | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Remote path. |
mode |
string | yes | Octal permission string, e.g. "644" or "755". |
Returns: { ok: true, path: string, mode: string }
symlink
Create a symbolic link at path pointing to target.
| Param | Type | Required | Description |
|---|---|---|---|
target |
string | yes | The path the link points to. |
path |
string | yes | Where to create the link. |
Returns: { ok: true, path: string, target: string }
End-to-end example (agent flow)
A typical "pull the latest release, verify, promote" flow chains a few tools:
list_files—path:/staging,wildcard:*.zip,sortField:modifyTime,sortDirection:desc→ newest build'spath.stat— that path → confirmsize/mode.download_file— that path → inspect the base64 contents.move—/staging/build.zip→/releases/build.zip,overwrite:true→ promote.
Credentials — 100% per-call, zero config
Every tool call names its own target and secret, so an agent can talk to any number of SFTP servers, switched call-to-call, with no registration and no restart. There is no server-side config.
The secret in a call is optional:
- Inline —
password, orprivateKey(+ optionalpassphrase). - Omitted → local SSH fallback — the server uses your ssh-agent / default
~/.sshkey (triesid_ed25519,id_ecdsa,id_rsa). Keeps private-key material out of the model. (Meaningful for stdio only — a remote container has no user agent.)
Security — documented, and it maps to the transport
- stdio = the safe mode for inline secrets. The server runs locally as a child of your client; the secret never leaves your machine. Use stdio (or the ssh-agent fallback) for anything sensitive.
- ⚠️ streamable HTTP behind a public tunnel — inline secrets transit the network and any proxy in front. Do not put inline passwords through a public endpoint. That is the one combination to avoid.
Built-in safe defaults (not config): a 32 MiB max transfer size (per-call overridable), no credential logging, and sanitized error messages (no stacks / internal paths leaked back to the model).
Hardening — read-only mode & path jail
Two launch flags (operator-set, not per-call) let you expose the server — e.g. over a shared streamable-HTTP endpoint — without handing agents full write access to every path a credential can reach.
| Flag | Env | Effect |
|---|---|---|
--read-only |
SFTP_READONLY=1 |
Refuse every mutating tool (upload_file, upload_dir, delete_file, delete_dir, make_directory, move, chmod, symlink) with error code EROFS. Read tools still work. |
--allow <root> |
SFTP_ALLOW=<root>[,<root>] |
Confine all paths to the given root(s). A path outside every root (after ./.. normalization) is refused with EACCES before any connection is made. Repeat --allow for multiple roots. |
# A safe public read-only endpoint:
npx @cordfuse/sftp-mcp --http --read-only
# Writable, but jailed to /exports:
npx @cordfuse/sftp-mcp --http --allow /exports
The flags compose — --read-only --allow /exports gives read-only access
confined to /exports. They constrain what the server will do regardless of
what credentials a call carries.
Responses, limits & errors
- Success — the tool returns a JSON object/array (the "Returns" shape above), serialized as text content.
- File contents are base64 —
download_file/download_filesreturnbase64;upload_filetakesbase64data. Binary flows through the model, so transfers are size-capped (32 MiB default, per-call overridable viamaxBytes/maxTotalBytes). For very large files this server is the wrong tool — it moves bytes through the agent, not disk-to-disk. - Errors — a failed op returns
isError: trueand a JSON body{ code, message }— a structured code so an agent can branch (rather than string-match), plus a sanitized, single-line message (credentials and stack traces are never included). Codes:ENOENT(no such path),EACCES(permission denied / outside the path jail),EEXIST(exists withoutoverwrite),EROFS(read-only mode),EISDIR,ENOTDIR,ENOSYS(unsupported extension, e.g.disk_usage),E2BIG(a batch/tree selection over the combined size cap — a single-file cap surfaces its message underEFAILURE),EINVAL(badmode),EFAILURE(fallback). - Connections are opened per call and always closed (even on error), with
the
timeoutMsready-timeout applied.
Transports
- stdio (default) — for Claude Code, Cursor, and local agent wiring. The safe mode for inline secrets, and the only mode where the ssh-agent fallback applies.
- streamable HTTP —
--http [--port N](orPORTenv; default3901). Stateful sessions; liveness probe atGET /health→{ "status": "ok" }. For remote / metamcp-style wiring.
Docker / GHCR
docker run -p 3901:3901 ghcr.io/cordfuse/sftp-mcp:latest
# or, from a checkout:
docker compose -f docker/compose.yaml up
Serves streamable HTTP at http://<host>:3901/mcp, liveness at /health. The
image omits ssh2's optional native cpu-features accelerator and runs ssh2's
pure-JS crypto — no native toolchain in the image.
Repository layout (monorepo)
sftp-mcp/
packages/
mcp/ @cordfuse/sftp-mcp (the server)
docker/ Dockerfile + compose.yaml (the --http server)
.github/ CI + release workflows
Development
npm ci
npm run build -w @cordfuse/sftp-mcp
# tests need a live SFTP server:
docker run -d --name sftp-test -p 2222:22 atmoz/sftp foo:testpass:1001::upload
npm test -w @cordfuse/sftp-mcp # override with SFTP_HOST/PORT/USER/PASS
CI / Release
- CI — typecheck + build + test on Node 20 & 22, against an
atmoz/sftpservice container. - Release — pushing a
v*tag publishes the npm package@cordfuse/sftp-mcp(guards tag == version) and the imageghcr.io/cordfuse/sftp-mcp:<version>+:latest.
Heritage: ports and modernizes
steve-krisjanovs/sftp-rest
(2023) — the SFTP domain logic — onto
ssh2-sftp-client and MCP.
License
MIT
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。