CLI tool
classfang/ssh-mcp-server avatar
classfang/ssh-mcp-server

ssh-mcp-server: letting an MCP client run commands over SSH

基于 SSH 的 MCP 服务 🧙‍♀️。已被MCP官方收录 🎉。 SSH MCP Server 🧙‍♀️. It has been included in the community MCP repository 🎉.

973 stars117 forksJavaScriptISC

At a glance

What is it?
ssh-mcp-server is an MCP server that gives an AI assistant the ability to run commands, move files and read and write text on remote hosts over SSH, while keeping the SSH credentials on the local side and gating what may be executed with regex allow and deny lists. It is the tool that makes an agent genuinely useful on a server, and the tool that makes an agent genuinely dangerous on one.
Who is it for?
Adopt ssh-mcp-server when you want an agent to work on infrastructure you already trust, on a dedicated account, with an allowlist that only permits read commands, and when the MCP client is one you control. It is the wrong tool for a production host that holds other people's data, for a machine where a mistaken command would be expensive, and for any setup where the credential has to live in the same configuration file the agent can read.
Can I use it commercially?
Yes. ISC is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A bridge between an agent and a shell

ssh-mcp-server is an MCP server built on SSH. Its stated purpose is to let applications that speak the Model Context Protocol, AI assistants among them, execute remote SSH commands over a standardised interface, so that an assistant can operate remote servers, run commands and get results back, without the SSH credentials being handed to the model.

That last clause is the design argument. In a naive arrangement, an agent that can run commands needs a password or a private key, and anything the agent can read, the model can be induced to read. This server keeps the connection on the local side, so the model receives command output rather than the means of authentication, and the README lists credential isolation as one of its features for exactly that reason.

The project is published on npm as @fangjunjie/ssh-mcp-server and hosted under the classfang organisation on GitHub, and the README states that it has been included in the community MCP repository. The code is JavaScript on the Model Context Protocol SDK, with ssh2 doing the SSH and SFTP work, socks for proxying and zod for validation, and the package is typed as an ES module with a compiled build directory as its only published artefact. The licence is ISC.

The audience is anyone running an agent against infrastructure they control and want to stop pasting commands by hand. The README is written in Chinese with an English version alongside it, and the tone is practical rather than promotional, which fits a tool whose whole job is to give a language model a shell.

There is also a WeChat group for support, which tells you the audience is largely Chinese speaking, and the tool ships a helper skill for agents that support the skills format, so the configuration can be produced by an interactive question and answer instead of by hand written JSON.

The tools the model actually gets

The tool list in the README is the whole capability surface, and it is broader than a single execute tool, which is the point. A model that can only run commands still needs a file transfer step, and a server that only runs one command at a time is useless for a long build.

The command execution tool runs an SSH command on the remote server and returns the result. The upload tool sends a local file or a whole directory to a location on the server, and the download tool does the reverse. The read-file tool reads a remote text file over SFTP and can page through it by byte offset, and it refuses binary files, which is a sensible guard against a model trying to read a database or an image through a text interface. The write-file tool writes text content directly, either overwriting or appending, and the README notes that it does so without going through shell quoting, which removes a whole class of escaping bugs from a generated file write.

The list-servers tool enumerates the configured SSH servers, so a model can ask what it is allowed to reach rather than guessing.

The background job tools are the most interesting group: job-start, job-status, job-kill and job-list. Their purpose is to run tasks that exceed the command timeout limit, poll for output and exit code, and terminate a job. Without them, a long operation would have to fit inside a single call, which is exactly the kind of constraint that pushes a user to raise timeouts to absurd values or to run the command in the background by hand.

Taken together, these are the operations a person would do over SSH: run a command, push a file, pull a file, look at a file, change a file, list what you can reach, and manage something long running. The model gets a shell with a safety rail, and the safety rail is the subject of the next section.

Configuration, and the one rule about the args array

The server runs through npx, so there is no install step, and the MCP client configuration is a JSON object. The README is organised by connection scenario, from the simplest to the most elaborate, and every example is a complete mcpServers entry that can be copied.

The password case looks like this:

json
{
  "mcpServers": {
    "ssh-mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "@fangjunjie/ssh-mcp-server",
        "--host", "192.168.1.1",
        "--port", "22",
        "--username", "root",
        "--password", "pwd123456"
      ]
    }
  }
}

The README puts a warning above the examples that is worth repeating, because it is the kind of thing that silently breaks a configuration: every command line argument and its value must be a separate element of the args array, so you write "--host", "192.168.1.1" and not "--host 192.168.1.1". The second form is a single string with a space in it, the server receives one argument, and the host never gets set.

The other examples in the sequence are the private key case, where --privateKey points at a key file, the key with a passphrase, where --passphrase supplies it, and the case where the host already exists in the SSH config file, where --host names the alias and everything else is read from your own configuration:

text
Host myserver
    HostName 192.168.1.1
    Port 22
    User root
    IdentityFile ~/.ssh/id_rsa

A custom configuration file can be pointed at with --ssh-config-file, and the README states the precedence rule explicitly: command line arguments win over the SSH config values, so a --port of 2222 overrides the port in the file. That ordering is the sensible one, since an explicit argument is a deliberate choice and the config file is the default.

The proxy option covers hosts that are only reachable through a jump. The --proxy flag accepts SOCKS5, HTTP or HTTPS, and the supported URL formats are given as:

text
socks://username:password@proxy-host:1080
socks5://username:password@proxy-host:1080
http://username:password@proxy-host:8080
https://username:password@proxy-host:8443

HTTP and HTTPS proxies tunnel to the SSH service with the CONNECT method and use Basic proxy authentication, and the default ports are 80 and 443 when they are omitted, while SOCKS5 requires an explicit port. The old socksProxy configuration and its --socksProxy argument remain compatible but accept only the socks schemes, and the two settings must not be used together.

The allow and deny lists, and how they are evaluated

This is the part of the project that decides whether it is safe to run, and the README is unusually explicit about it.

The --whitelist and --blacklist options restrict which commands may run on the server. Multiple patterns are separated by commas and each pattern is a regular expression. The README says in strong terms that this is strongly recommended for production.

The whitelist matches the complete command, and when a whitelist is in force, a command containing a semicolon, an ampersand, a pipe, a backtick, a command substitution, a redirection or a newline is rejected outright. That is a structural defence rather than a pattern defence: even a whitelist that happens to permit a command cannot be extended with a chain, a pipe, a redirect or a substitution, so there is no way to smuggle a second command past it.

The example whitelist is a read only set, permitting a directory listing, a file read and a disk usage query:

json
{
  "mcpServers": {
    "ssh-mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "@fangjunjie/ssh-mcp-server",
        "--host", "192.168.1.1",
        "--username", "root",
        "--password", "pwd123456",
        "--whitelist", "^ls(?: [A-Za-z0-9_./-]+)*$,^cat [A-Za-z0-9_./-]+$,^df(?: -[A-Za-z]+)*(?: [A-Za-z0-9_./-]+)?$"
      ]
    }
  }
}

The evaluation order is worth internalising. When both lists are given, the command is first checked against the whitelist for a complete match, then checked against the blacklist, and it has to pass both. The README recommends always writing explicit anchors in the whitelist patterns so that they can be read and audited, and it says that if several operations have to be combined, the logic belongs in a separately reviewed script rather than in a command string.

There is a sharp edge in the blacklist side. The patterns are case sensitive by default, so a pattern of ^shutdown.* does not stop Shutdown /a. The README's workaround is to write character classes such as [sS] instead of relying on an inline case insensitive flag, because older Node.js versions do not support the (?i:pattern) syntax and bare (?i) is not valid in JavaScript regular expressions at all. An invalid pattern makes the server fail to start, which is a fail closed behaviour and still a bad afternoon.

One more detail catches people out: backslashes have to be escaped in the JSON string, so the regular expression \s is written as \\s in the JSON, and the pattern is written to tolerate whitespace before the command.

Transports, bastion hosts and long running work

The default transport is stdio, which is what the MCP clients that launch a local process expect. Adding --mcp-transport http exposes an MCP endpoint over Streamable HTTP instead, which matters when the server is not a child process of the client, for example when it runs in a container or on another machine while the client runs on a laptop.

The SSH side has two transports of its own, exec and shell, and the README frames them as covering both direct connections to a host and the case where the target is behind a bastion or jump host. That distinction is not cosmetic in SSH, because an interactive shell session behaves differently from a single exec channel, and a tool that has to run a command and read its output reliably wants the exec form.

The background job tools exist because of timeouts. The README describes them as a way to start long tasks that exceed the command timeout limit, then poll the output and the exit code, and to terminate a job. The job-kill tool is the interesting one from a safety point of view, because it gives the model a way to stop something it started, which is a meaningful difference between a tool that can only start processes and one that can manage them.

The helper skill, ssh-mcp-helper, sits alongside the server and covers the same configuration ground through an interactive flow: check the Node.js environment, choose an MCP client, choose an authentication method, collect the connection parameters, and write the configuration. The README says it supports every scenario described in the document, including password and key authentication, reuse of the SSH config, SOCKS proxying, bastions, multiple connections, two factor authentication and command restrictions, and that it produces correctly formatted configuration.

The reason this matters is that the configuration is long and the failure mode is silent. A malformed args array produces a server that starts and cannot connect, and a mistyped password produces an authentication failure in the log, and having an assistant assemble the file from a question and answer session removes a class of error that has nothing to do with the agent's reasoning.

Adoption, the licence and the risk you are taking on

The licence is ISC, which is permissive and short, and the repository is not archived with the last push on 2026-10-15. The release history is steady: v1.10.0 on 2026-10-09, v1.9.2 on 2026-09-09 and v1.9.0 on 2026-08-18. The version number moving as fast as that suggests a project that is still finding its shape, and it is worth reading the notes for the release you pin.

The runtime dependencies are small and legible: the Model Context Protocol SDK, ssh2, socks and zod. The test runner is the Node built-in test runner, with a scripts/run-tests.js wrapper for the full suite, and the package is built with a scripts/build.js that TypeScript compiles into the build directory that is the only thing published.

The real discussion is about what this tool does to your risk profile, and it is worth being blunt.

The tool gives a language model a shell on a remote machine. Everything the model can be talked into, the model can attempt, and a model is talked into things by content it reads. A help page, a log file, a web page, a comment in a pull request: any of those can carry an instruction. The whitelist exists precisely for that, and the structural rejection of chaining characters is the acknowledgement that pattern matching alone is not enough.

So the sensible configuration is a dedicated account with no privileges, an allowlist rather than a deny list, a host you can rebuild, and a file transfer capability you have thought about, because the upload and download tools move data in both directions and the read-file tool pages through content the model will then read. Putting a production database server behind this, with the credentials in the same JSON file the process can read, is the configuration to avoid.

There is also a credential hygiene point. The example configurations put the password in plain text in the JSON, which is how these files are usually written, and the SSH config reuse path exists partly to avoid that. The proxy support accepts a proxy URL with a username and password in it, which is another secret in the same place. Nothing here is unusual for MCP servers, and the ISC licence makes the code easy to fork and change, which is the right answer for a tool you want to run with a policy of your own.

Editorial conclusion

Adopt ssh-mcp-server when you want an agent to work on infrastructure you already trust, on a dedicated account, with an allowlist that only permits read commands, and when the MCP client is one you control. It is the wrong tool for a production host that holds other people's data, for a machine where a mistaken command would be expensive, and for any setup where the credential has to live in the same configuration file the agent can read. Before enabling it, start with the --whitelist option rather than the --blacklist one, because a deny list has to enumerate every dangerous command rather than every permitted one, and check the order of evaluation, since a command must pass both lists to run.

Frequently asked questions

What does the ssh-mcp-server MCP server do?

It lets an MCP client such as an AI assistant run commands, upload and download files, read and write text files, list the configured servers and manage background jobs on remote hosts over SSH. The SSH credentials stay on the local side and are not passed to the model.

How do I restrict which commands an agent can run on my server?

Use the --whitelist and --blacklist options with comma separated regular expression patterns. The whitelist matches the complete command, and when a whitelist is enabled, commands containing semicolons, ampersands, pipes, backticks, command substitution, redirection or newlines are rejected outright. A command must pass both checks to run.

How do I configure the MCP client for ssh-mcp-server?

Add an mcpServers entry whose command is npx and whose args array contains -y, @fangjunjie/ssh-mcp-server and the options, with each argument and its value as a separate array element. The README warns explicitly against joining a flag and its value into one string.

Can ssh-mcp-server reuse my existing SSH config?

Yes. If the host alias exists in your SSH config, the server reads the connection parameters from it, and --ssh-config-file points at a different file. Command line arguments take precedence over the values in the SSH config.

What transports does the server support?

The MCP transport defaults to stdio, and adding --mcp-transport http exposes an MCP endpoint over Streamable HTTP. On the SSH side it supports both the exec and shell transports, which covers direct hosts as well as bastion and jump host arrangements.

Official sources

  1. classfang/ssh-mcp-server on GitHub
  2. Issues
  3. License: ISC
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/classfang-ssh-mcp-server.svg)](https://hysenlabs.com/projects/classfang-ssh-mcp-server)

Was this useful?