Skip to main content
Glama
cordfuse

sftp-mcp

by cordfuse

sftp-mcp

CI npm

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-mcp treats 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

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) or password (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 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 }


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:

  1. list_files — path:/staging, wildcard:*.zip, sortField:modifyTime, sortDirection:desc → newest build's path.

  2. stat — that path → confirm size / mode.

  3. download_file — that path → inspect the base64 contents.

  4. 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, or privateKey (+ optional passphrase).

  • Omitted → local SSH fallback — the server uses your ssh-agent / default ~/.ssh key (tries id_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_files return base64; upload_file takes base64data. Binary flows through the model, so transfers are size-capped (32 MiB default, per-call overridable via maxBytes / 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: true and 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 without overwrite), 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 under EFAILURE), EINVAL (bad mode), EFAILURE (fallback).

  • Connections are opened per call and always closed (even on error), with the timeoutMs ready-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] (or PORT env; default 3901). Stateful sessions; liveness probe at GET /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/sftp service container.

  • Release — pushing a v* tag publishes the npm package @cordfuse/sftp-mcp (guards tag == version) and the image ghcr.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

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables LLMs to interact with remote servers via SSH, supporting command execution, file upload/download, and directory listing.
    7
    MIT
  • A
    license
    B
    quality
    D
    maintenance
    Enables AI agents to perform FTP/FTPS/SFTP file operations including upload, download, sync, and directory management with multi-server support.
    36
    44 npm
    5
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to securely execute commands on remote hosts via SSH and SFTP, with persistent shells, file transfers, screenshots, and an audit log.
    4
    MIT