sftp-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@sftp-mcplist all files in /home/user/documents"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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
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.
Related MCP server: lms-ssh-client-mcp
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 |
| string | yes | — | SFTP server hostname or IP. |
| string | yes | — | SSH username. |
| number | no |
| TCP port. |
| string | no | — | Inline password. |
| string | no | — | Inline private key (PEM / OpenSSH text). |
| string | no | — | Passphrase for an encrypted |
| number | no |
| 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 |
| string | yes | — | Remote directory to list. |
| string | no | — | Glob on the entry name, e.g. |
|
| no | all | Keep only these entry types. |
|
| no | — | Field to sort by. |
|
| no |
| Sort direction. |
| boolean | no |
| Descend into subdirectories. |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| string | yes | — | Remote file path. |
| number | no |
| 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 |
| string | yes | — | Remote directory. |
| string | no | — | Glob filter, e.g. |
| boolean | no |
| Include matching files in subdirectories. |
| number | no |
| 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 |
| string | yes | — | Destination path (including filename). |
| string | yes | — | File contents, base64-encoded. |
| boolean | no |
| Replace an existing file. Without it, an existing path is an error. |
| number | no |
| 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 |
| string | yes | — | Remote base directory (created if missing). |
|
| yes | — | Files to write; each |
| number | no |
| 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 |
| string | yes | Remote file to delete. |
Returns: { ok: true, deleted: string }
delete_dir
Remove a directory.
Param | Type | Required | Default | Description |
| string | yes | — | Remote directory to remove. |
| boolean | no |
| Remove contents too ( |
Returns: { ok: true, removed: string, recursive: boolean }
make_directory
Create a directory.
Param | Type | Required | Default | Description |
| string | yes | — | Remote directory to create. |
| boolean | no |
| Create missing parents ( |
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 |
| string | yes | — | Current path. |
| string | yes | — | Destination path. |
| boolean | no |
| Replace an existing destination. Without it, an existing |
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 |
| string | yes | Remote path. |
| string | yes | Octal permission string, e.g. |
Returns: { ok: true, path: string, mode: string }
symlink
Create a symbolic link at path pointing to target.
Param | Type | Required | Description |
| string | yes | The path the link points to. |
| 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 |
|
| Refuse every mutating tool ( |
|
| Confine all paths to the given root(s). A path outside every root (after |
# A safe public read-only endpoint:
npx @cordfuse/sftp-mcp --http --read-only
# Writable, but jailed to /exports:
npx @cordfuse/sftp-mcp --http --allow /exportsThe 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 upServes 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 workflowsDevelopment
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/PASSCI / 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
This server cannot be deployed
Maintenance
Related MCP Connectors
Persistent file storage for AI agents via MCP and curl. Upload, download, and version files.
- flockfsOAuthcom.flockfs
A real-time, multiplayer filesystem for agents and humans: shared files, live edits, history.
File uploads for AI agents. Upload, list, and manage files. No signup required.
Artifact store for AI agents — read, write, and search files by path; share by rendered URL.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables LLMs to interact with remote servers via SSH, supporting command execution, file upload/download, and directory listing.7MIT
- FlicenseAqualityCmaintenanceEnables LLMs to securely SSH into remote servers, execute commands, and manage files via SFTP including listing, reading, writing, deleting, and renaming files.11-
- AlicenseBqualityDmaintenanceEnables AI agents to perform FTP/FTPS/SFTP file operations including upload, download, sync, and directory management with multi-server support.3624 npm5MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to securely execute commands on remote hosts via SSH and SFTP, with persistent shells, file transfers, screenshots, and an audit log.5MIT