Server data from the Official MCP Registry
Work with a remote host over FTP/FTPS/SFTP, SSH, and MySQL.
Work with a remote host over FTP/FTPS/SFTP, SSH, and MySQL.
This is a well-designed MCP server for remote file transfer and shell execution with strong security posture. The codebase demonstrates thoughtful security practices: explicit opt-in for dangerous capabilities (SSH execution, database access), path confinement to configured base directories, shell metacharacter filtering to prevent command injection, and proper secret handling. Minor code quality findings around error handling breadth and logging practices do not materially impact security. Supply chain analysis found 2 known vulnerabilities in dependencies (2 critical, 0 high severity). Package verification found 1 issue.
3 files analyzed · 8 issues found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
This plugin requests these system permissions. Most are normal for its category.
Set these up before or after installing:
Environment variable: FTP_HOST
Environment variable: FTP_PORT
Environment variable: FTP_USER
Environment variable: FTP_PASSWORD
Environment variable: FTP_BASE_DIR
Environment variable: FTP_SECURITY
Environment variable: FTP_TLS_REJECT_UNAUTHORIZED
Environment variable: FTP_TIMEOUT_MS
Environment variable: SSH_HOST
Environment variable: SSH_PORT
Environment variable: SSH_USER
Environment variable: SSH_PASSWORD
Environment variable: SSH_PRIVATE_KEY
Environment variable: SSH_PASSPHRASE
Environment variable: SSH_HOST_FINGERPRINT
Environment variable: SSH_BASE_DIR
Environment variable: SSH_TIMEOUT_MS
Environment variable: SSH_MAX_OUTPUT
Environment variable: SSH_ALLOWED_CMDS
Add this to your MCP configuration file:
{
"mcpServers": {
"io-github-chris221-ftp-ssh-mcp": {
"env": {
"DB_USER": "your-db-user-here",
"FTP_HOST": "your-ftp-host-here",
"FTP_PORT": "your-ftp-port-here",
"FTP_USER": "your-ftp-user-here",
"SSH_HOST": "your-ssh-host-here",
"SSH_PORT": "your-ssh-port-here",
"SSH_USER": "your-ssh-user-here",
"REMOTE_HOST": "your-remote-host-here",
"REMOTE_USER": "your-remote-user-here",
"FTP_BASE_DIR": "your-ftp-base-dir-here",
"FTP_PASSWORD": "your-ftp-password-here",
"FTP_SECURITY": "your-ftp-security-here",
"SSH_ACTIVATE": "your-ssh-activate-here",
"SSH_BASE_DIR": "your-ssh-base-dir-here",
"SSH_PASSWORD": "your-ssh-password-here",
"FTP_TIMEOUT_MS": "your-ftp-timeout-ms-here",
"SSH_MAX_OUTPUT": "your-ssh-max-output-here",
"SSH_PASSPHRASE": "your-ssh-passphrase-here",
"SSH_TIMEOUT_MS": "your-ssh-timeout-ms-here",
"REMOTE_BASE_DIR": "your-remote-base-dir-here",
"REMOTE_PASSWORD": "your-remote-password-here",
"SSH_PRIVATE_KEY": "your-ssh-private-key-here",
"SSH_ALLOWED_CMDS": "your-ssh-allowed-cmds-here",
"SSH_HOST_FINGERPRINT": "your-ssh-host-fingerprint-here",
"FTP_TLS_REJECT_UNAUTHORIZED": "your-ftp-tls-reject-unauthorized-here"
},
"args": [
"-y",
"ftp-ssh-mcp"
],
"command": "npx"
}
}
}From the project's GitHub README.
An MCP server for working with a single remote host over FTP, FTPS, SFTP, and SSH — the kind of host a typical shared-hosting plan or a plain VPS gives you, not a cloud provider's API. It groups its tools into three capability classes: file transfer (list, upload, download, mkdir, delete), shell execution over SSH, and a MySQL query wrapper that also runs over SSH. File transfer is the only capability enabled by default — shell execution and database access are both opt-in and stay off until you explicitly turn them on. Treat those two as equally dangerous: mysql_query runs arbitrary SQL on a production database and can escape to a shell, so it is not the milder of the pair.
Published to npm and run with npx — there is nothing to clone or build. Requires Node 20 or newer. Works on Windows, macOS and Linux.
On Windows, use the
cmd /cform shown for each client below. Most clients spawncommanddirectly rather than through a shell, andnpxon Windows is a.cmdshim that cannot be executed that way — the server simply fails to start, usually with no useful error. Treat this as the rule on Windows, not an edge case.
{
"mcpServers": {
"remote": { "command": "npx", "args": ["-y", "ftp-ssh-mcp@1"] }
}
}
Windows:
{
"mcpServers": {
"remote": { "command": "cmd", "args": ["/c", "npx", "-y", "ftp-ssh-mcp@1"] }
}
}
| OS | Path |
|---|---|
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Linux | ~/.config/Claude/claude_desktop_config.json |
Same mcpServers shape as Claude Code. Claude Desktop has no project directory, so set MCP_ENV_FILE to an absolute path — see step 2.
Same mcpServers shape as Claude Code.
VS Code uses servers, not mcpServers:
{
"servers": {
"remote": { "command": "npx", "args": ["-y", "ftp-ssh-mcp@1"] }
}
}
Codex uses TOML with a snake_case mcp_servers table:
[mcp_servers.remote]
command = "npx"
args = ["-y", "ftp-ssh-mcp@1"]
Windows:
[mcp_servers.remote]
command = "cmd"
args = ["/c", "npx", "-y", "ftp-ssh-mcp@1"]
Or add it from the shell: codex mcp add remote -- npx -y ftp-ssh-mcp@1
Create a .env file in your project root. The server reads it from the working directory, which is the project root for project-scoped clients:
REMOTE_HOST=ftp.example.com
REMOTE_USER=deploy
REMOTE_PASSWORD=your-password
REMOTE_BASE_DIR=/home/deploy/public_html
That is enough for the six file tools over FTPS. Keep this file out of version control — add .env and .env.* to .gitignore. Getting credentials out of a tracked config file is the reason the server reads .env at all.
If your client has no project directory (Claude Desktop), or you want several servers against different accounts, point each at its own file instead:
{
"mcpServers": {
"prod": {
"command": "npx",
"args": ["-y", "ftp-ssh-mcp@1"],
"env": { "MCP_ENV_FILE": "/absolute/path/to/.env.prod" }
}
}
}
A fuller setup, adding shell execution and database access — both off unless you opt in:
REMOTE_HOST=example.com
REMOTE_USER=deploy
REMOTE_PASSWORD=your-password
REMOTE_BASE_DIR=/home/deploy/public_html
# Pin the host key. Without this, any host key is accepted — see Security.
SSH_HOST_FINGERPRINT=SHA256:your-fingerprint-here
SSH_PORT=22
SSH_BASE_DIR=/home/deploy
SSH_ALLOW_EXEC=true
SSH_ALLOWED_CMDS=@basic,@node
# Runs the mysql client on the host over SSH. At least as dangerous as
# SSH_ALLOW_EXEC — read the Security section before enabling.
DB_USER=deploy_dbuser
DB_PASSWORD=your-db-password
DB_NAME=deploy_db
# Off by default; required for file_delete.
REMOTE_ALLOW_DELETE=true
See .env.example for every variable and Configuration below for what each does.
npx -y ftp-ssh-mcp@1 --selftest
Run it from the directory holding your .env. It resolves configuration, registers tools and prints a one-line summary — without opening a connection — then exits. Use it to confirm the right capabilities came up before wiring the server into a client:
ftp-ssh-mcp 1.0.1 selftest OK. env=.env, transport=ftp, host=example.com,
capabilities=[files, ssh, mysql], tools=[file_list, file_upload, ...]
It exits non-zero with a message naming the missing variable if configuration is incomplete. See Troubleshooting.
| Option | Does |
|---|---|
--selftest | Resolve the configuration, print the tools it would register, and exit. Opens no connection. |
-q, --quiet | Drop the informational startup warnings. |
-v, --version | Print the version and exit. |
-h, --help | Print usage and exit. |
--version and --help answer before any configuration is read, so they work on a machine with nothing set up yet. Both print to stdout; everything else the server says goes to stderr, because stdout is the JSON-RPC channel.
--quiet silences the warnings about a missing DB_PASSWORD, an MCP_CAPABILITIES entry that registered no tools, and a relative base directory. It deliberately does not silence the two that say your security posture is weaker than you might assume — an unverified host key, or path confinement switched off. A flag set once in a client's config file is never looked at again, so those stay visible until you fix them, which is the only silence worth having.
A capability's tools are only registered when it is both configured (its required variables are set) and, if MCP_CAPABILITIES restricts the set, named in that list.
| Capability | Tools | Enabled when |
|---|---|---|
files | file_list, file_upload, file_upload_dir, file_download, file_mkdir, file_delete | An FTP or SSH profile is configured. file_delete additionally requires REMOTE_ALLOW_DELETE=true; all writes require REMOTE_READONLY to be unset or false. |
ssh | ssh_exec | An SSH profile is configured with SSH_BASE_DIR set and SSH_ALLOW_EXEC=true. |
mysql | mysql_query | An SSH profile is configured, and DB_USER and DB_NAME are both set. DB_USER must be set explicitly — it does not inherit REMOTE_USER — so the capability cannot switch itself on. Blocked entirely by REMOTE_READONLY=true. DB_PASSWORD is not required — see below. |
The files tools are transport-neutral: they work over whichever transport FILE_TRANSPORT selects (ftp or sftp), and each call can override the transport for that one call if both profiles are configured. mysql_query is a convenience wrapper, not a database driver — it pipes SQL over SSH to the mysql client already installed on the host (mysql --user=... --database=... --table, with the SQL sent on stdin), then parses the CLI's table output. There is no connection pooling and no parameterised-query support. It exists because most shared hosts will not accept a database connection from outside the host itself; if you need a real client-side database integration, use one instead of mysql_query. Enable it with the same caution as ssh_exec, not less — see Security.
Every setting is resolved from environment variables in this order: a profile-specific variable (FTP_* or SSH_*) first, then the shared REMOTE_* fallback, then a built-in default. Two independent profiles exist — FTP and SSH — and you may configure either, both, or neither in isolation (though at least one is required to start).
Variables are normally read from process environment, but the server also loads a .env file from the current working directory before resolving configuration, so credentials can live outside the MCP client's own (often tracked) config file. A real environment variable already set takes precedence over the .env file. Point at a different file with MCP_ENV_FILE — a leading ~ is expanded to your home directory, a relative value is resolved against the working directory, and a set-but-unreadable one fails startup rather than silently falling back to .env and loading different credentials than the ones you named. The .env parser does not support inline comments — KEY=value # note treats everything after = as the value, including # note, so keep comments on their own line.
Non-secret settings (HOST, USER, BASE_DIR) fall back to REMOTE_* freely. Secrets (PASSWORD, PRIVATE_KEY, PASSPHRASE) fall back to REMOTE_PASSWORD only when the profile does not set its own *_USER. A profile that names its own user is declaring a distinct identity, and sending that identity a password meant for a different account causes repeated authentication failures — on many shared hosts, enough failures trip brute-force protection and lock the account out at the host level. If FTP_USER (or SSH_USER) is set, its password must be set explicitly too.
| Variable | Default | Notes |
|---|---|---|
REMOTE_HOST | — | Shared host, used when a profile does not set its own. |
REMOTE_USER | — | Shared user. |
REMOTE_PASSWORD | — | Shared secret. Only inherited by a profile with no *_USER of its own. |
REMOTE_BASE_DIR | — | Shared base directory that remote paths are confined to. A leading ~ is expanded to the account's login directory — see below. |
FTP_HOST | inherits REMOTE_HOST | |
FTP_PORT | 21 | |
FTP_USER | inherits REMOTE_USER | |
FTP_PASSWORD | see secret inheritance | |
FTP_BASE_DIR | inherits REMOTE_BASE_DIR, then SSH_BASE_DIR | Base directory the files tools are confined to on this transport. Accepts ~ or ~/<dir>. |
FTP_SECURITY | ftps | ftps (explicit TLS), ftp (plaintext), or ftps-implicit. |
FTP_TLS_REJECT_UNAUTHORIZED | true | Set false to accept a self-signed or otherwise unverifiable certificate. |
FTP_TIMEOUT_MS | 30000 | |
SSH_HOST | inherits REMOTE_HOST | |
SSH_PORT | 22 | |
SSH_USER | inherits REMOTE_USER | |
SSH_PASSWORD | see secret inheritance | Password auth. Can be combined with a key; the server will try both. |
SSH_PRIVATE_KEY | see secret inheritance | Local path to a private key. A leading ~ is expanded. |
SSH_PASSPHRASE | see secret inheritance | Passphrase for SSH_PRIVATE_KEY. |
SSH_HOST_FINGERPRINT | inherits REMOTE_HOST_FINGERPRINT | SHA-256 host key fingerprint to pin. Accepts the SHA256:<base64> form or a bare hex digest. Not a secret, so it inherits freely. Unset means any host key is accepted, and the server warns at startup. See Security below. |
SSH_BASE_DIR | inherits REMOTE_BASE_DIR, then FTP_BASE_DIR | Required for ssh_exec and mysql_query, which both need a known working directory. Also the confinement root for files calls on the sftp transport. Accepts ~ or ~/<dir>. |
SSH_ACTIVATE | — | Remote path to a script (e.g. a Node virtualenv's activate) sourced before every command. Missing file is non-fatal — the command still runs. |
SSH_TIMEOUT_MS | 120000 | Connection and per-command timeout. |
SSH_MAX_OUTPUT | 100000 | Combined stdout+stderr byte cap per command; output beyond this is truncated, not buffered. |
SSH_ALLOWED_CMDS | @basic,@node,@mysql | Comma-separated allowlist of program names ssh_exec may invoke. Entries starting with @ are presets — see the table below. The default expands to the same twelve commands the old literal default listed. |
DB_USER | — | Must be set explicitly. Unlike other non-secrets it does not inherit REMOTE_USER, so DB_NAME alone cannot activate mysql_query. |
DB_PASSWORD | see secret inheritance | See below — not required. |
DB_NAME | — | Default database for mysql_query; can be overridden per call. |
FILE_TRANSPORT | ftp if an FTP profile is configured, else sftp if an SSH profile is configured, else ftp | Which profile serves the files tools. |
MCP_CAPABILITIES | all configured capabilities | Comma-separated allowlist restricting which capabilities register tools (files, ssh, mysql). An unrecognised name fails startup. |
REMOTE_READONLY | false | When true, blocks every write across file transfer, ssh_exec and mysql_query (uploads, deletes, mkdir, shell commands and SQL alike). FTP_READONLY, the pre-1.0 name, still works; a set REMOTE_READONLY wins. |
REMOTE_ALLOW_DELETE | false | Must be true for file_delete to work. FTP_ALLOW_DELETE, the pre-1.0 name, still works; a set REMOTE_ALLOW_DELETE wins. |
SSH_ALLOW_EXEC | false | Must be true for ssh_exec to be registered at all. |
SSH_ALLOWED_CMDS entries starting with @ expand to a named group; presets and literal command names mix freely (SSH_ALLOWED_CMDS=@basic,@php,rsync). An unknown preset name fails startup. Presets are named after stacks, not hosting products, and expand as follows:
| Preset | Commands |
|---|---|
@basic | ls, cat, tail, head, df, du, pwd, touch |
@node | node, npm |
@php | php, composer |
@mysql | mysql, mysqldump |
A DB_USER with no resolvable DB_PASSWORD is not treated as an error: the remote mysql client can legitimately get credentials from ~/.my.cnf or a trusted local socket instead of a password on the connection. The server prints a warning to stderr at startup rather than refusing to run, and if a query then fails to authenticate, mysql_query's error message names DB_PASSWORD again so the cause is visible from inside the MCP client, not just in a startup log you may not have seen.
~ and ~/<dir> are accepted and expanded to the account's own login directory: realpath(".") over SFTP, PWD over FTP, for the files tools. That lookup happens on connect, and only when there is a ~ to expand, so an absolute base directory costs nothing extra and the server still needs no network at startup.
ssh_exec and mysql_query resolve the same setting a different way: the raw ~ is handed to the remote login shell, which expands $HOME itself on every command, so there is no connect-time lookup on that path at all. The two mechanisms agree in practice, since they resolve against the same logged-in account — only when and how the expansion happens differ.
Each profile expands its own value against its own login directory. That is deliberate: on many shared hosts the FTP account is chrooted so that its ~ is /, while SSH sees the real /home/<user> — so a ~/public_html shared through REMOTE_BASE_DIR lands in the right place on both.
A named home, ~other/site, is refused at startup — but only when the files tools are registered; a config that excludes them (e.g. MCP_CAPABILITIES=ssh) starts fine and only fails later, the first time ssh_exec or mysql_query tries to cd there. Only the account that logged in can be located, so a named home would be sent to the server as a literal path. Use an absolute path for that case.
ssh_exec only runs a program named in SSH_ALLOWED_CMDS, checked against the first whitespace-delimited token of the command — not a policy over arguments.
Before that check, the command is rejected outright if it contains any of a fourteen-character shell metacharacter class (; & | $ ( ) { } < > \ and newline/carriage return), so a call can only ever be a single plain invocation — no chaining, redirection, or substitution.
Remote paths passed to the files tools and to SSH_BASE_DIR/FTP_BASE_DIR are resolved and confined to the profile's configured base directory; .. segments are rejected outright. Confinement is per profile, and a per-call transport override is confined to the profile it selects, not to the default one. A profile with no base directory at all is not confined — there is nothing to confine it to — so each profile falls back to the other's base directory when it has none of its own, and the server warns on stderr at startup for any transport that still ends up without one.
Deletes (REMOTE_ALLOW_DELETE) and shell execution (SSH_ALLOW_EXEC) are both off by default and must be turned on explicitly.
Pin the SSH host key with SSH_HOST_FINGERPRINT. Unless it is set, the SSH and SFTP connections accept whatever host key they are offered — there is no known_hosts to fall back on — so anything on the network path can present its own key and be handed the account's password. When the variable is set, a mismatched key aborts the connection with an error naming both the expected and the received fingerprint; when it is unset, the server says so in a startup warning on stderr. Get the value from the host you trust with:
ssh-keyscan -t rsa your-host.example.com | ssh-keygen -lf -
and copy the SHA256:… field (a bare hex digest is also accepted). A fingerprint that cannot be parsed is a startup error rather than a silent fallback to "no verification".
mysql_query never builds a shell string from your SQL — the query is written to the mysql client's stdin, so it is never interpolated into a command line.
DB_PASSWORD never appears in the host's process list. It is delivered as the first line of the command's stdin, read into MYSQL_PWD by the remote shell and exported to the mysql process — so no argv anywhere carries it, and ps aux shows only the harmless command text. The secret exists only in process environments, which are owner-readable. One constraint follows from the framing: a password containing a line break is refused with an explicit error. When DB_PASSWORD is unset, no password mechanism runs at all, so host-side credentials such as ~/.my.cnf genuinely take over.
mysql_query is at least as powerful as ssh_exec, not a lesser grant. It runs arbitrary SQL against a production database, and the mysql client's \! command is a shell escape that is honoured on piped input — so \! curl … | sh reaches a shell regardless of SSH_ALLOW_EXEC and SSH_ALLOWED_CMDS. The two capabilities are separate switches because they are separate features, not because one is safer; enable mysql with the same care as ssh.
None of this makes the remote host a sandbox. An allowed command still runs with the full privileges of whichever account is configured, and there is no isolation between what ssh_exec can do and what that account could do logged in directly. The guards constrain the shape of a single call — one program, no shell tricks, paths that stay inside a base directory — they do not constrain what an allowed program itself is capable of once it runs.
Found a security problem? Please report it privately — see SECURITY.md.
npm install ftp-ssh-mcp currently reports two moderate advisories for
@hono/node-server, pulled in transitively by @modelcontextprotocol/sdk. They
concern path traversal in that package's static-file server on Windows. This
server talks JSON-RPC over stdio and never starts an HTTP listener, so that code
path is not reachable here, and the only "fix" npm offers is a breaking downgrade
of the SDK. CI runs npm audit --omit=dev --audit-level=high, which passes today
while still failing the build on anything high or critical. The advisories will
clear when the SDK updates its dependency.
Start with npx -y ftp-ssh-mcp@1 --selftest from the directory holding your .env. It reproduces everything the server does at startup except connecting, so it separates a configuration problem from a network or credentials one.
| Symptom | Cause |
|---|---|
| Server never starts, no error in the client | On Windows, command is npx. Use the cmd /c form — see Install. |
No connection profile configured | No host or user resolved. Set REMOTE_HOST and REMOTE_USER, or the FTP_*/SSH_* equivalents. |
env=<none> in the selftest output | No .env file was found and MCP_ENV_FILE is unset. .env is resolved from the working directory, not from the config file's location or the package's. Set MCP_ENV_FILE to an absolute path if your client has no project directory. |
MCP_ENV_FILE is "…", but that file could not be read | The explicitly named env file does not exist or is not readable at that path (resolved against the working directory when relative). This fails startup deliberately — a typo'd path must not silently start the server on whatever .env happens to be in the working directory. |
<PREFIX>_PASSWORD is not set even though REMOTE_PASSWORD is | The profile sets its own *_USER, so it is treated as a distinct identity and does not inherit the shared secret. Set its password explicitly — see Secret inheritance. |
| A tool you expected is missing | Its capability is not enabled. ssh_exec needs SSH_ALLOW_EXEC=true and SSH_BASE_DIR; mysql_query needs DB_USER and DB_NAME, with DB_USER set explicitly. Check capabilities=[…] in the selftest line. |
Selftest says capabilities=[] | MCP_CAPABILITIES names capabilities that are not configured. It can only narrow what is already configured, never enable anything. |
Value from .env has a trailing comment in it | The parser has no inline-comment support. Put comments on their own line. |
| Warning about an unverified host key | SSH_HOST_FINGERPRINT is unset. Expected until you pin it, and not a failure — see Security. |
| Warning that path confinement is disabled | The transport's profile has no base directory and neither does the other one. Set REMOTE_BASE_DIR. |
| Warning that the base directory is relative | The value has no leading / and is not ~-relative — often a dropped leading slash, as in home/u/site. It still works, because the server resolves it against wherever the session starts, but the root is then the server's choice rather than one pinned at connect. Write ~/site for the account's own home, or an absolute path. |
| Uploads land somewhere unexpected | Remote paths are relative to the profile's base directory, and each profile has its own. A per-call transport override resolves against that profile's base directory, not the default one. |
A ~ base directory resolves somewhere unexpected | ~ is the login directory the server reports, not /home/<user> by definition. A chrooted FTP account reports /, so ~/public_html is /public_html there while SSH resolves the same value to /home/<user>/public_html. Both are correct for their account; set that profile's own *_BASE_DIR to an absolute path if you need to override it. |
git clone https://github.com/Chris221/ftp-ssh-mcp.git
cd ftp-ssh-mcp
npm install
npm test
npm run selftest
npm test runs the Vitest suite, including capability-registration and transport tests against an in-process FTP server. npm run selftest resolves configuration from the environment (and .env, if present), registers tools, and prints a one-line summary — including the resolved transport and which capabilities registered — without opening a connection or starting the stdio transport.
Be the first to review this server!
by Modelcontextprotocol · File & Storage
Secure file operations with configurable access controls
by Toleno · Developer Tools
Toleno Network MCP Server — Manage your Toleno mining account with Claude AI using natural language.
by mcp-marketplace · Developer Tools
Create, build, and publish Python MCP servers to PyPI — conversationally.