SSH MCP Server
It provides controlled, policy-enforced SSH access to remote hosts for LLM agents, exposing tools to run commands, manage sessions, transfer files, and signal processes.
Discover configured hosts and connection status (
list-connections)List active sessions (
list-sessions)Open interactive (stateful) or background (
tail -f-style) sessions, then close them (open-session,close-session)Read output from background sessions (
read-session-output)Run allowlisted read-only commands like
ls,cat,grep(read-command)Run arbitrary shell commands, with destructive ones gated by an approval policy (
run-command)Execute commands with sudo, piping the password via stdin to avoid process-list leaks (
privileged-command)Upload files to and download files from remote hosts via SFTP (
sftp-upload,sftp-download)Send INT/TERM/KILL signals to remote PIDs (
signal-process)Enforce role/tier-based policy, denylists, command quotas, optional OPA sidecar checks, human-in-the-loop approval, and full audit logging
Enables remote execution of shell commands and administrative tasks with sudo elevation on Linux systems via secure SSH connections.
Click on "Install 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., "@SSH MCP ServerCheck the CPU usage and list the current running processes"
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.
SSH MCP Server v2
SSH MCP Server is a security-first Model Context Protocol server that gives LLM agents controlled SSH access to remote hosts — with command classification, policy-based authorization, human-in-the-loop approval, and full audit logging.
The risk this server exists to manage. Giving an LLM shell access on a remote host puts private data, untrusted input and network egress in one place — Simon Willison's "lethal trifecta". Prompt injection has no general fix, so ssh-mcp assumes any command may be attacker-influenced: it classifies before executing, authorizes against a role × host-group matrix, gates destructive work behind approval, and records the decision either way. That narrows the blast radius; it does not remove the risk. Two things stay yours: never point it at a root account, and never set
autoapproval on a production profile. SECURITY.md has the full threat model.
Quick Start
1. Install
npm install -g ssh-mcp2. Configure
Without a config the server still starts, so a client or directory can complete the MCP
handshake and read tools/list — but every tool call is refused until you configure it,
with a message naming the path below. Nothing runs on a host until this step is done.
Create the config file at the path for your platform:
Platform | Path |
Linux |
|
macOS |
|
Windows |
|
[defaults]
defaultProfile = "dev"
approvalMode = "ask-destructive"
[[profiles]]
name = "dev"
host = "192.168.1.100"
port = 22
user = "deploy" # NOT root!
auth = "key"
keyRef = "~/.ssh/id_ed25519"
role = "admin"
approvalPolicy = "auto" # dev is permissivechmod 700 ~/.config/ssh-mcp && chmod 600 ~/.config/ssh-mcp/config.tomlThe config decides which hosts, roles and policy rules this server honours, so it checks that nobody but you can read it — and treats the two platforms differently, because the question has a much clearer answer on one of them.
Linux and macOS: enforced. The mode check above, on the file and the directory —
which is why chmod 700 is in that command, since mkdir -p under the default umask
leaves the directory 0755. The server refuses to start otherwise. "Only the owner" is
unambiguous here and chmod is a one-line fix.
Windows: split by what the ACL actually allows. There are no mode bits, so the ACL is read instead — and read exposure and write exposure are not treated alike, because Windows is much clearer about one of them than the other.
The ACL lets another account… | Default |
only read the config | reported, and the server starts |
change the config | refused |
nothing (no ACL at all) | refused — that is full control for everyone |
…and if the ACL could not be read | refused, except when |
A config under %APPDATA% inherits access for you, SYSTEM and Administrators and needs
nothing done to it. One created elsewhere does not: a file under C:\ inherits *read* for
every local account and *modify* for every authenticated one. The message names the two
icacls commands that fix it either way.
Read exposure is reported rather than refused because that is where Windows is genuinely muddier than POSIX, and refusing over it blocked a config at the documented location (#138). Write exposure is refused because it is not muddy at all: another account being able to rewrite the file that decides which hosts, roles and approval policy this server honours is an authorization bypass, not a disclosure.
Two flags move the whole thing: --strictConfigAcl refuses everything the check objects
to, read-only grants included; --allowUncheckedConfigAcl reports everything and refuses
nothing. Neither combination leaves you without an exit, which is the lesson of #138.
Exit statuses
Status | Meaning |
| Clean shutdown |
| A defect in the server — printed with a stack trace; please report it |
| How it was invoked or configured — printed as a message, no stack |
A supervisor that treats any non-zero status as a failure needs no change. One
that matched on 1 to detect a startup problem should match on 2 as well.
Starting with nothing configured is not an exit-2 condition, as of the release that
added introspection without a config: the server starts so it can be described, and
refuses each tool call instead. A supervisor that used a non-zero exit to catch an
unconfigured deployment should watch for starting unconfigured on stderr, or read
configured from GET /health when running the HTTP transport.
3. Set credentials via environment variables
export SSH_MCP_PASSWORD="your-password" # if using auth=password
# OR use SSH agent (recommended):
export SSH_AUTH_SOCK="$SSH_AUTH_SOCK" # already set if agent running4. Connect from your MCP client
Claude Code:
claude mcp add --transport stdio ssh-mcp -- ssh-mcpClaude Desktop / Cursor / Windsurf:
{
"mcpServers": {
"ssh-mcp": {
"command": "ssh-mcp",
"env": {
"SSH_MCP_PASSWORD": "your-password"
}
}
}
}Never pass passwords as CLI arguments — they're visible via ps aux. Use env vars, config files, SSH agent, or OS keychain.
Related MCP server: SSH MCP Server
Tools (11)
Tool | Purpose | readOnly | destructive |
| Discover available hosts and connection status | ✅ | — |
| List active sessions per host | ✅ | — |
| Create a named interactive (stateful) or background session | — | — |
| Close a session. A background session's command is signalled (INT/TERM/KILL) before its channel is dropped | — | ✅ |
| Read output from background sessions (e.g., | ✅ | — |
| Execute allowlisted read-only commands ( | ✅ | — |
| Execute arbitrary commands (destructive/privileged need approval, unless | — | — |
| Execute with sudo (needs approval, unless | — | ✅ |
| Upload a file via SFTP | — | ✅ |
| Download a file via SFTP | ✅ | — |
| Send INT/TERM/KILL to a remote PID | — | ✅ |
Interactive Sessions
Sessions maintain state (CWD, environment variables) between commands:
Agent: open-session(name="deploy", type="interactive")
Agent: run-command(session="deploy", command="cd /opt/myapp")
Agent: run-command(session="deploy", command="git pull") # runs in /opt/myapp
Agent: run-command(session="deploy", command="npm ci") # CWD persists
Agent: close-session(name="deploy")Background Sessions
Long-running processes (logs, builds):
Agent: open-session(name="logs", type="background", command="tail -f /var/log/syslog")
Agent: read-session-output(name="logs", lines=20) # poll
Agent: close-session(name="logs")Remote host support
Tested against Linux (Debian/bash, Alpine/busybox ash), Dropbear, and Windows OpenSSH on Windows 11.
Linux / BSD / macOS | Windows OpenSSH | |
| ✅ | ✅ |
| ✅ | ✅ |
Background sessions | ✅ | ✅ |
Interactive sessions | ✅ | ❌ |
Interactive sessions require a POSIX shell (sh, bash, ash, zsh). They work by
bracketing each command with printf markers and reading $? and $PWD from a
trailer — none of which exist in cmd.exe, the default shell for Windows
OpenSSH. Opening one against such a host fails immediately with an explicit
error rather than timing out; everything else works normally.
Setting PowerShell as the OpenSSH DefaultShell does not help: the protocol is
POSIX-specific, not merely non-cmd.
Configuration
Profile options
[defaults]
defaultProfile = "dev"
sessionMaxPerConnection = 5
sessionIdleTimeoutMs = 600000 # 10min
sessionBackgroundMaxMs = 3600000 # 1hr
commandTimeoutMs = 60000
commandMaxChars = 5000 # 0 = unlimited, the config spelling of --maxChars=none
commandMaxOutputBytes = 1048576 # 1MB
connectionIdleReapMs = 900000 # 15min
commandQuotaPerDay = 0 # 0 = unlimited; circuit breaker for runaway agents
approvalGrantTtlMs = 0 # 0 = always prompt; see "Approval Grants"
approvalMode = "ask-destructive" # auto | ask-destructive | ask-all | deny
[[profiles]]
name = "prod-web-1"
host = "10.0.1.50"
port = 22
user = "deploy"
auth = "agent" # agent | key | password | keychain
keyRef = "~/.ssh/id_ed25519" # for auth=key
keychainEntry = "ssh-mcp/prod" # for auth=keychain (requires @napi-rs/keyring)
via = "bastion" # ProxyJump — route through bastion profile
group = "prod" # Policy tier: prod | staging | dev, or your own (see [policy])
workdir = "/var/www"
trustedHostKey = "SHA256:..." # Pin host key (optional)
tty = false
role = "operator" # viewer | operator | admin
readOnly = false
approvalPolicy = "ask-all"
cert = false # SSH CA cert auth — auto-detects keyRef-cert.pub
sessionMaxPerConnection = 3 # per-profile override
sessionIdleTimeoutMs = 300000 # stricter for prod
commandQuotaPerDay = 200 # per-profile override
maxChars = 2000 # per-profile override; stricter for prod
# Optional. Merged over the built-in role matrix; see "Policy Engine" below.
# roleBindings is keyed by role and then by tier, so the block below changes
# operator on prod and leaves operator's other tiers, and viewer and admin,
# on their defaults.
[policy]
denylist = ["^terraform\\s+destroy"]
[policy.roleBindings.operator]
prod = ["read-only", "safe", "destructive"]Unknown sections and keys are a startup error, not a warning, so a typo cannot
leave you running defaults you thought you had overridden. That extends to role
and tier names: every one you write under [policy.roleBindings] has to be
reachable by some profile, and every profile's role and tier has to resolve to
real bindings. Both directions are checked at startup.
ProxyJump (Bastion)
Reach internal hosts behind a bastion/jump server. The via field specifies a profile name to tunnel through:
[[profiles]]
name = "bastion"
host = "bastion.example.com"
user = "deploy"
auth = "agent"
[[profiles]]
name = "internal-db"
host = "10.0.1.50" # private IP — not directly reachable
user = "dbadmin"
auth = "key"
keyRef = "~/.ssh/db_key"
via = "bastion" # tunnel through bastionNo agent forwarding — only a TCP tunnel via forwardOut. The bastion stays connected and reusable for multiple internal hosts.
SSH CA Certificates
For enterprise setups with a central SSH Certificate Authority:
[[profiles]]
name = "prod-db"
host = "db.internal"
user = "admin"
auth = "key"
keyRef = "~/.ssh/id_ed25519"
cert = true # enable CA cert authThe certificate file is auto-detected using OpenSSH convention (keyRef + -cert.pub, e.g. ~/.ssh/id_ed25519-cert.pub). You can override the path with SSH_MCP_<NAME>_CERT env var. The cert is concatenated with the private key per ssh2 convention.
Credential Resolution Order
SSH agent (
SSH_AUTH_SOCK) — no key material in process memoryOS keychain (macOS Keychain / Windows Credential Manager / Linux Secret Service) — requires
auth = "keychain"and@napi-rs/keyringEnvironment variables —
SSH_MCP_PASSWORD,SSH_MCP_KEY,SSH_MCP_SUDO_PASSWORD, or profile-specificSSH_MCP_<NAME>_PASSWORDKey file —
keyRefpath orSSH_MCP_KEYenv var
Never CLI arguments. v2 removes --password, --sudoPassword, --suPassword entirely.
Policy Engine
Roles
Role | Dev | Staging | Prod |
viewer | read-only | read-only | read-only |
operator | read-only, safe, destructive | read-only, safe, destructive | read-only, safe |
admin | all | all | read-only, safe, destructive |
Which column applies comes from the profile's group. Set it explicitly —
without it the tier is guessed from the profile name (prod/staging/dev,
local, test, sandbox), and an unrecognised name resolves to prod,
the strictest tier. A production host named web-01 is therefore treated as
production rather than silently getting dev permissions.
Note what this means for sudo: admin has no privileged on prod, so
privileged-command is refused there by design — including on a quick-start
profile, which has no name to infer from and therefore lands on prod. If the
host is not production, say so:
npx ssh-mcp --host=10.0.0.5 --user=deploy --group=dev[[profiles]]
name = "build-box"
group = "dev"Configuring the matrix
The table above is the default, not a limit. An optional [policy] section is
merged over it at startup, so granting sudo on a host you have honestly
labelled prod is a reviewable line in a config file rather than a relabelling:
[policy.roleBindings.admin]
prod = ["read-only", "safe", "destructive", "privileged"]The merge is at role and tier depth. That block changes admin on prod and
nothing else: admin on staging and dev keep their defaults, and viewer
and operator are untouched. Roles and tiers the defaults have never heard of
are added rather than rejected, which is what makes a custom group resolve to
real bindings instead of falling back to the strictest tier:
[[profiles]]
name = "build-box"
role = "admin"
group = "tier-1"
[policy.roleBindings.admin]
"tier-1" = ["read-only", "safe", "destructive"]Extra deny patterns live in the same section, and are applied on top of the never-allowed list rather than replacing it:
[policy]
denylist = ["^terraform\\s+destroy"]Because role and tier names are free strings, nothing in the merge itself can tell a new custom role from a misspelling of an existing one. A cross-check at startup does, and these all fail there rather than at the point of use:
a command class outside
read-only | safe | destructive | privileged, so apriviledgedtypo cannot parse into a grant of nothing and then read as a policy decision when a command is refused;any unrecognised section or key anywhere in the config, so a block the parser does not understand is an error rather than a clean startup with none of the behaviour you configured;
a role or tier under
[policy.roleBindings]that no profile uses, so[policy.roleBindings.operater]cannot merge in as a fourth role while the profiles you meant to restrict keep running on defaults;a profile whose
rolehas no bindings, or whose tier has none under that role, so a host cannot end up onread-onlyfor a reason nobody wrote down.
The last one covers the tier you did not set as well as the one you did. A
profile with no group still resolves to one by name, and that inferred tier
has to exist under the profile's role like any other.
A tier with no bindings for a role grants read-only, and never another tier's
classes. There is no fallback between tiers: while the matrix was compiled in,
falling back to prod meant falling back to a role's strictest cell, but a
[policy] block can write that cell now.
An OPA sidecar is not an alternative route to the same grant. OPA is consulted only for commands the local policy already allows, so it can refuse more but never widen. Widening happens here or not at all.
Command Classification
Every command is classified before execution:
read-only: Allowlisted commands (
ls,cat,grep,df,stat,systemctl status, ...)safe: Non-destructive mutations (
npm install,git pull, ...)destructive: mutations that need approval (
rm -rf /tmp/build, ...)privileged:
sudo,su,doas,pkexec
A separate forbidden list is never allowed, whatever the role or approval
policy: rm -rf /, mkfs, dd of=/dev/, shutdown, curl|sh, fork bombs,
writes to /etc/cron, /etc/systemd or authorized_keys, iptables -F, and
recursive chmod 777 / / chown /. Add your own patterns via the policy
denylist; an invalid pattern fails at startup rather than degrading silently.
Approval Modes
auto— no prompts (dev only!)ask-destructive— prompt for destructive/privileged (default). Narrower than it sounds: outside the never-allowed list,destructiveis onerm -rf /pathpattern,findwith a write/exec flag, an unresolvable command word, a program handed to an interpreter this server cannot read (python3 -c,perl -e,node -e, a program arriving on a pipe — but notawk, whose program is not read), andsftp-upload/interactiveopen-session— elevation classifiesprivileged, which also prompts. Ordinary writes, service control and signals do not. See SECURITY.md before relying on this in production.ask-all— prompt for every commanddeny— reject destructive/privileged commands outright (no prompt)
Approval Grants (just-in-time)
approvalGrantTtlMs lets one explicit approval cover repeats of the exact
same command on the same profile for a bounded time (e.g. 300000 for five
minutes). It exists because approving rm -rf /tmp/build every few seconds
during an iterative task trains you to click through prompts — which is worse
for safety than a grant you chose deliberately.
A grant is bound to the exact command text, the profile and the command class:
approving rm -rf /tmp/build does not cover rm -rf /tmp/build-prod, the same
command on another host, or the same command escalated to sudo. Runs covered
by a grant appear in the audit log with approver: "jit-grant", so they stay
distinguishable from a fresh human answer.
Off by default (0 = always prompt). Auto-approval weakens the gate that
makes destructive commands safe, so turning it on should be a decision.
Answering the prompt
Approval goes through the MCP elicitation request, so what you see is your client's dialog. Accepting it approves the command — there is no second field to fill in.
You have 10 minutes to answer. Past that the request expires and the command
is refused rather than left pending, and the refusal says so; the prompt may
still be open in your client, in which case run the command again once you are
ready. If your client does not support elicitation at all, every destructive and
privileged command is refused with APPROVAL_UNAVAILABLE naming that cause —
approval fails closed by design.
Command Quota
commandQuotaPerDay bounds how many commands a profile may run in a rolling
24-hour window (0 = unlimited). The approval gate stops destructive commands
and the HTTP rate limiter caps request rate, but neither bounds total work — a
prompt-injected agent looping over allowed commands stays under both. The quota
is the circuit breaker for that case.
Counted after policy allows a command and before it runs, so a denied command does not spend budget. The window slides rather than resetting at midnight, which would let an agent spend a full quota just before the reset and another immediately after.
External Policy Engine (OPA)
For organizations that standardize on Open Policy Agent / Rego:
ssh-mcp --opaUrl=http://localhost:8181When --opaUrl is set, commands the built-in engine allows are additionally
evaluated by OPA. OPA can only narrow. A command the built-in engine has
already denied returns that denial without OPA being consulted at all, so a
sidecar answering allow cannot grant a class the role bindings withhold. To
widen, edit [policy].
An outage falls back to the local decision and logs one warning per minute. That is the
default because OPA is an additional deny layer and stopping all work would be the worse
failure — but an operator who deployed OPA as the authorization gate loses that gate
during the outage, and the only signal is a stderr line MCP clients usually discard.
--opaFailClosed makes the gate being down mean no; the refusal carries ruleId: "opa-unavailable" so the audit record says the gate was down rather than implying a policy
refused the command.
The request shape follows the AuthZEN Access Evaluation contract:
{
"input": {
"subject": { "role": "operator", "profile": "prod-web-1" },
"action": { "tool": "run-command", "commandClass": "destructive" },
"resource": { "command": "rm -rf /tmp/cache", "binary": "rm", "host": "10.0.1.50" },
"context": { "readOnly": false }
}
}OPA responds with { "result": true/false }. If OPA denies (result: false), the command is blocked even if the built-in engine allows it. If OPA is unreachable, the built-in engine's decision stands by default (fail-open, to avoid locking out access); --opaFailClosed refuses instead. A 200 that carries no boolean result counts as unreachable — that is what OPA answers for an undefined document, so a misnamed package or an unactivated bundle is an outage rather than consent.
Example Rego policy (ssh-mcp.rego):
package ssh.mcp
default allow := false
# Admins pass the OPA gate on dev hosts. The built-in policy still applies on
# top: this widens nothing that the role bindings withhold.
allow if {
input.subject.role == "admin"
startswith(input.subject.profile, "dev")
}
# Deny all destructive commands on prod
deny if {
input.action.commandClass == "destructive"
startswith(input.subject.profile, "prod")
}Security
Threat Model
See SECURITY.md for the full threat model, vulnerability reporting policy, and deployment checklist.
Supply chain
Releases carry signed attestations, published through Sigstore and recorded in its public transparency log. They live in two different stores, which is what decides how each is verified:
Attestation | Predicate | Stored by | Since |
Build provenance — SLSA Build Level 2 |
| npm | every release |
SBOM — CycloneDX and SPDX |
| GitHub | releases after v2.4.0 |
Provenance comes from npm trusted publishing: the release workflow authenticates with a short-lived OIDC token and no stored credential, so there is no long-lived npm token to leak.
npm audit signatures # provenance, against an installed tree
npm pack ssh-mcp # the SBOM attestation is bound to the tarball, so fetch it
gh attestation verify ssh-mcp-*.tgz --repo tufantunc/ssh-mcp --predicate-type https://cyclonedx.org/bomBoth flags on the last command are load-bearing. gh attestation verify defaults to the
SLSA predicate, so without --predicate-type it filters the SBOM out and reports nothing
found — and the provenance it would look for instead is in npm's store, not the GitHub
store --repo queries. Use https://spdx.dev/Document for the SPDX one.
Both SBOMs are also attached to each GitHub release, for reading rather than verifying.
Level 2, not 3. Provenance is signed by the generic GitHub-hosted runner —
builder.id is https://github.com/actions/runner/github-hosted — which the build itself
can influence; Build L3 requires an isolated builder it cannot. Reaching L3 is not
currently compatible with trusted publishing: npm turns on its own provenance whenever that
setting is left at its default, and then ignores any externally generated one. So L3 today
would mean returning to a long-lived npm token — trading the property described above for a
level number.
Safe Defaults
Non-root user in all examples
TOFU host key verification (accept on first connect, verify after — within one process; see SECURITY.md)
RFC 9142 algorithm allow-list (no SHA-1, no CBC, no ssh-rsa)
exec()-only (no persistent su shells — fixes PTY leak)
Sudo via stdin (not argv — fixes process list leak)
Sanitizer strips CR/LF/NUL from all metadata
3-layer redaction (field → regex → entropy) on audit logs
No CLI-arg secrets (use env vars, keychain, or config)
Hardening Checklist
Create dedicated low-privilege service account on target hosts
Use command-specific
sudoersinstead ofNOPASSWD: ALLEnable
ask-allapproval for production profilesRestrict network egress on target hosts
Use
readOnly = truefor monitoring profilesReview audit logs regularly
Run
chmod 700 <config dir> && chmod 600 config.toml(Windows: the ACL under%APPDATA%is already restricted)
Transports
stdio (default)
For local MCP clients (Claude Code, Cursor, Windsurf). No network exposure.
ssh-mcp # reads config from XDG path
ssh-mcp --config=/path/to.toml # custom config pathHTTP (optional)
For remote/web clients behind a reverse proxy with TLS:
ssh-mcp --transport=http --httpPort=3000 --bearerToken=secret
ssh-mcp --transport=http --httpPort=3000 --bearerToken=secret --rateLimit=60Flag | Default | Description |
| required | Bearer token for authentication (all routes except |
| 3000 | HTTP listen port |
| 127.0.0.1 | Bind address |
| 0 (off) | Max requests per minute (0 = unlimited) |
| 10 | Failed bearer-auth attempts allowed per client per minute (0 = off) |
| false | Read the client address from |
| — | Comma-separated peer addresses allowed to send |
Endpoints: POST / (MCP Streamable HTTP), GET /status, GET /health
GET /health answers {"healthy": true, "configured": <bool>}. It stays 200 either way —
healthy is liveness — while configured is false when no profile is set, which is the
case of a config bind mount that silently did not attach: the server binds the port and
refuses every tool call. GET /status carries the profile list itself and stays behind the
bearer token.
When rate limit is exceeded, the server returns HTTP 429 with Retry-After header and a JSON-RPC error body so MCP clients can handle it gracefully.
Failed authentication is throttled separately, and on by default. --rateLimit never saw a
wrong bearer token, because the auth check answers before the limiter is reached — so
guessing ran at network speed. --authFailureLimit gives each client its own small budget,
spent only on a 401; a correct token never consumes from it, so a working client never
throttles itself. Once an address has spent its budget every request from it waits,
including one with the right token — that is deliberate, since answering the guess would
otherwise tell the caller which token was right. Clients are told apart by socket address. Behind a
reverse proxy that means every client shares one budget, so set --trustProxy when the
proxy is yours — the server prints a warning the first time it sees X-Forwarded-For
without it. --trustProxy takes the rightmost X-Forwarded-For entry, which is the hop
the proxy itself appended; everything to its left came from the client, so reading the
leftmost would let a client choose its own budget or spend a victim's. That only holds if a
proxy actually appended the entry, so the header is read only when the peer is the
proxy — bare --trustProxy means a loopback peer, which is the deployment above; name a
proxy elsewhere with --trustedProxies. When the header cannot be read as an address, or
the peer is not trusted, the server falls back to the socket address and says so once, so
a flag that is not taking effect is not silent. One trusted hop is assumed. A malformed --authFailureLimit is refused at startup rather than silently
disabling the check; only 0 turns it off.
Always terminate TLS at a reverse proxy (Caddy/nginx). The server listens on 127.0.0.1 only.
Docker
# Build
docker build -t ssh-mcp .
# Run (config file + env vars for credentials)
docker run -i \
-v ./config.toml:/home/appuser/.config/ssh-mcp/config.toml:ro \
-e SSH_MCP_PASSWORD=secret \
ssh-mcpOr with docker-compose:
docker-compose --profile app upThe Docker image runs as non-root UID 65532, with a minimal node:22-slim base.
CLI Flags (v2)
Secrets are never passed as CLI arguments.
Flag | Default | Description |
| platform config dir (see Configure) | Path to TOML config file |
| — | Quick start: SSH host (creates single-profile config) |
| — | Quick start: SSH username |
| 22 | Quick start: SSH port |
| — | Quick start: Path to private key |
| — | Quick start: Working directory for commands and sessions |
| prod | Quick start: Policy tier — |
| 60000 | Command timeout in ms |
| 5000 | Max command length ( |
| 5 | Max concurrent sessions per connection |
| 600000 | Session idle timeout in ms |
| stdio |
|
| 3000 | HTTP transport port |
| 127.0.0.1 | HTTP bind address |
| — | Bearer token for HTTP transport auth (required for |
| 0 | HTTP requests per minute on the MCP route (0 = unlimited) |
| 10 | Failed bearer-auth attempts allowed per client per minute (0 = off) |
| false | Read the client address from |
| — | Comma-separated peer addresses allowed to send |
| bind address + localhost | Comma-separated Host headers accepted by the DNS-rebinding guard |
|
|
|
| false | Disable host key verification for hosts with no |
| false | Windows: report every ACL finding and refuse none |
| false | Windows: refuse on every ACL finding, including a read-only over-grant |
| false | Skip the approval gate (quick start profile only) |
| — | OPA sidecar URL for external policy |
| false | Refuse every command while OPA is unreachable, instead of falling back to local policy |
| 10000 | How long to wait for the OPA sidecar. Lower makes the fail-open cheaper to reach; higher makes an outage slower to notice |
| 0 (off) | Max commands per rolling 24h per profile |
| 0 (off) | Auto-approve an identical command for this many ms after approval |
| false | Enable entropy-based secret scanning in audit |
| false | Enable hash-chained tamper-evident audit log |
| — | OTLP/HTTP endpoint for OpenTelemetry traces |
| ssh-mcp | Service name reported on trace spans |
| — | Print SHA-256 hashes of the tool descriptions and exit |
Migrating from v1
v2 is a breaking release. Passing a removed flag now fails at startup with the replacement, rather than failing later as a confusing auth error.
Tools
v1 | v2 | Notes |
|
| Allowlisted read-only commands. Prefer this for reads. |
|
| Arbitrary commands. Destructive and privileged ones go through the approval gate, unless |
|
| Requires approval unless |
| — | Removed. It was an injection vector (#44) and never reached the host. |
Command results now carry status. In v1 a failed command rejected with
Error (code N). In v2 a non-zero exit comes back as an error result including
the exit code and stderr — so an empty response no longer means "it worked".
Flags
v1 flag | Replacement |
|
|
|
|
|
|
| Use a role/policy that disallows the |
Credentials moved off the command line because CLI arguments are world-readable
via /proc/<pid>/cmdline on Linux (CWE-214). Credentials now resolve through an
SSH agent → OS keychain → env var → key file cascade.
Example
// v1
{ "command": "npx", "args": ["ssh-mcp", "--host=1.2.3.4", "--user=root", "--password=hunter2"] }
// v2 — credentials via env
{
"command": "npx",
"args": ["ssh-mcp", "--host=1.2.3.4", "--user=root"],
"env": { "SSH_MCP_PASSWORD": "hunter2" }
}For more than one host, move to a TOML config file (see Configuration)
and pass --config <path>; profiles carry per-host roles and approval policy.
Host key verification
v1 did not verify host keys. v2 defaults to trust-on-first-use and records the
key in memory, for the life of the process; a later mismatch in that same
process fails the connection. Nothing is written to disk and ~/.ssh/known_hosts
is not consulted, so a restart accepts afresh — see
SECURITY.md. Pin
explicitly with trustedHostKey in a profile, which is the only control here that
survives a restart — and which no host key mode overrides, so --insecureHostKey
is an opt-out only for hosts you have not pinned (test environments only).
Testing
# Start test SSH server
docker-compose --profile test up -d
# Run all tests
npm test
# Run only unit tests
npm test -- test/unit/
# Run with coverage
npm run coverageMCP Inspector
npm run inspectContributing
See CONTRIBUTING.md. Please follow the security checklist in all PRs.
Support
If you find SSH MCP Server helpful, consider starring the repository or sponsoring!
Listed on
Also on the official MCP registry as io.github.tufantunc/ssh-mcp.
Available Tools
11 toolsclose-sessionADestructive
Close a named session. A background session's command is signalled on the host (INT, then TERM, then KILL) before its channel is dropped; an interactive session's shell is ended. The response says so if the command could not be signalled or had not stopped in time.
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Session name to close | |
| profile | No | Profile name |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the destructiveHint annotation, the description discloses the full shutdown sequence for background sessions (INT, then TERM, then KILL), that the channel is dropped, that interactive shells are ended, and that the response indicates failures to signal or stop in time. This is substantial behavioral context that annotations alone do not provide.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences with no filler. The core action is front-loaded, followed by essential behavioral details and response semantics. Every sentence contributes information an agent needs.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a destructive operation with no output schema, the description covers the shutdown behavior, the difference between session types, and how failures are reported. Given the annotations and schema, nothing critical is missing for correct invocation and interpretation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both parameters are already documented. The description adds the context of 'named session' but does not add new meaning to the parameters themselves. Baseline 3 is appropriate when the schema fully carries the parameter documentation burden.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb and resource: 'Close a named session,' which precisely states what the tool does. It further distinguishes behavior for background versus interactive sessions, separating it from list-only or read-only sibling tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The tool's use case is implied: call it when you need to close a named session. However, it does not explicitly state when to use this over alternatives like signal-process or run-command, nor does it provide exclusion criteria. The context is clear but the guidance is not explicit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list-connectionsARead-only
List all configured SSH profiles and their connection status. Use this to discover available hosts before running commands.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, so the safe-read nature is covered. The description adds that the tool lists profiles and connection status, which clarifies what data is returned but does not elaborate on side effects, permissions, or what 'connection status' means. This is modest additional context beyond the annotation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences with no filler: the first states the action and scope, the second explains when to use it. The key information is front-loaded and every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter, read-only list tool with no output schema, the description is complete: it names what is listed, the status attribute, and the intended use case. An agent can correctly decide to invoke this tool and know what to expect without additional guidance.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool accepts zero parameters and schema description coverage is 100% (vacuously). Per the baseline for zero-parameter tools, no further parameter documentation is required. The description' s functional overview is sufficient for an agent to understand there a re no inputs.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb ('List') with a clear resource ('all configured SSH profiles') and an additional attribute ('their connection status'). It distinguishes from sibling 'list-sessions' by focusing on profiles rather than active sessions, and adds the intent of discovering available hosts before running commands.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit usage context: 'Use this to discover available hosts before running commands.' It clearly implies this is a preliminary, read-only discovery step. However, it does not name alternatives like 'list-sessions' or state when not to use this tool, which would have made the guidance stronger.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list-sessionsBRead-only
List active sessions for a given SSH profile.
| Name | Required | Description | Default |
|---|---|---|---|
| profile | No | Profile name (uses default if omitted) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The readOnlyHint annotation already declares this as a read-only operation, and the description's 'List' wording is consistent. The description adds the scope qualifier 'for a given SSH profile' but does not explain what 'active' means or how the tool behaves with an invalid profile. This is minimal added value beyond the annotation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single concise sentence with the verb front-loaded and no redundant words. It communicates the essential action and scope immediately and efficiently.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple one-parameter, read-only listing tool, the description covers the main selection and invocation needs: what it lists and for which scope. However, it omits guidance on the distinction from list-connections or the shape of the result, which would help an agent use it correctly. It is nearly complete but not fully self-contained.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The only parameter 'profile' is fully described in the schema ('Profile name (uses default if omitted)'). The description's phrase 'for a given SSH profile' restates the parameter without adding new meaning. Since schema coverage is 100%, the baseline of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('List') and resource ('active sessions') with a scope ('for a given SSH profile'), so the purpose is clear. It does not explicitly contrast with the sibling list-connections, so an agent must infer the distinction between sessions and connections. The verb+resource is specific but sibling differentiation is implicit.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no guidance on when to use this tool versus alternatives like list-connections or open-session. The description only restates what the tool does; it does not provide context or exclusions. An agent has no explicit direction for selecting this tool over its siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
open-sessionA
Open a named session on a remote host. Use type="interactive" for stateful shell (CWD/env persists between commands) or type="background" for long-running processes.
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Session name (alphanumeric, dash, underscore, max 64 chars) | |
| type | No | Session type | interactive |
| command | No | Command for background sessions | |
| profile | No | Profile name (uses default if omitted) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description adds useful behavior context: interactive sessions preserve CWD/env between commands, and background sessions are for long-running processes. However, with no annotations, it does not disclose lifecycle behavior such as whether sessions persist until explicitly closed, what happens on name collisions, or cleanup requirements.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, no filler, and the core open-session purpose is front-loaded before type guidance. Every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The core semantics are covered, but with no output schema and no annotations, the agent is left guessing about return values, session lifecycle, and how this relates to close-session or run-command. The background command requirement is also not fully clarified.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3. The description adds meaningful semantics beyond the schema by explaining when to use interactive vs. background and what stateful behavior means. It does not enrich name or profile parameters, but the type clarification is valuable.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a specific verb ('Open'), a specific resource ('named session'), and a target ('remote host'). It also distinguishes session types, which differentiates this from siblings like list-sessions, read-session-output, and close-session.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives clear choice guidance for the type parameter: interactive for stateful shell, background for long-running processes. It does not explicitly contrast with run-command or close-session, so it stops short of full alternative-based routing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
privileged-commandADestructive
Execute a command with sudo elevation. Goes through the approval gate; approvalPolicy on the profile decides whether that is a prompt, an automatic allow, or a refusal. The sudo password is piped via stdin (never visible in process list).
| Name | Required | Description | Default |
|---|---|---|---|
| command | Yes | Command to execute with sudo | |
| profile | No | Profile name |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the destructiveHint annotation by disclosing the approval gate behavior, the three possible approval outcomes, and the secure handling of the sudo password via stdin. No contradiction with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three focused sentences, each earning its place. The core action is front-loaded, followed by approval-gate behavior and a security-relevant implementation detail with no wasted words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a two-parameter tool with a destructive hint and no output schema, the description covers purpose, elevation, approval behavior, and password handling. It is slightly thin on expected return/output behavior, but that is not critical given the schema and annotations.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3. The description adds meaningful context by explaining that the profile's approvalPolicy controls prompt/allow/refusal behavior, which enriches the minimally documented 'profile' parameter.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: 'Execute a command with sudo elevation.' The sudo qualifier clearly differentiates this from sibling tools like run-command or read-command, so an agent can tell its purpose without opening schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides clear context: it is for commands requiring sudo elevation and that go through the approval gate. It does not explicitly name alternatives or state when not to use it, but the elevation context makes the intended use unambiguous.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
read-commandARead-only
Execute a READ-ONLY command from an allowlist (ls, cat, grep, find, stat, df, etc.). This tool does NOT modify the system. Prefer this tool for all read operations.
| Name | Required | Description | Default |
|---|---|---|---|
| command | Yes | Read-only shell command (must be in the allowlist) | |
| profile | No | Profile name |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true. The description adds the allowlist restriction, example commands, and reinforces the no-modification guarantee, which is useful behavioral context beyond the annotation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, both dense and relevant. The core READ-ONLY and no-modification traits are front-loaded, followed by a usage preference. No fluff.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple read-only tool with annotations and full parameter coverage, the description covers the purpose, usage, and constraint profile. It does not describe return values or error behavior, but with no output schema and familiar commands, this is acceptable.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema documents the command and profile parameters. The description adds example allowlist entries but does not introduce meaningfully new semantic detail beyond the schema's own mention of the allowlist.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific action ('Execute a READ-ONLY command from an allowlist'), names example commands, and explicitly claims no system modification. This clearly distinguishes it from write-capable siblings like run-command or privileged-command.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives an explicit usage rule: 'Prefer this tool for all read operations.' It implicitly excludes writes by emphasizing READ-ONLY, but it does not name a specific alternative sibling for write operations. Clear context, minor omission of explicit exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
read-session-outputARead-only
Read recent output from a background session (e.g., tail -f logs).
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Background session name | |
| lines | No | Number of recent lines to read | |
| profile | No | Profile name |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The readOnlyHint annotation already establishes that this is a read-only operation, and the description does not contradict it. It adds context about targeting background-session output but does not disclose return format, ordering, or potential edge cases.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single front-loaded sentence with a helpful example and no filler. Every part contributes to understanding the tool's purpose.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple read-only tool with fully documented parameters and a readOnlyHint, the description is adequate for basic invocation. It could mention prerequisites like an active session or output format, but those are not critical for a tool this straightforward.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema fully documents 'name', 'lines', and 'profile'. The description adds no parameter-level meaning beyond the tail-like example, keeping this at the baseline of 3.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the verb 'read' and the resource ('recent output from a background session'), which identifies the tool's purpose. It does not explicitly distinguish itself from the sibling read-command, so it falls short of a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The example 'tail -f logs' and the phrase 'background session' imply when this tool should be used, but there is no explicit guidance about alternatives or exclusions. It provides usable context without clear routing among sibling tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
run-commandADestructive
Execute an arbitrary shell command on the remote server. May modify the system. Commands classified destructive or privileged go through the approval gate; approvalPolicy on the profile decides whether that is a prompt, an automatic allow, or a refusal.
| Name | Required | Description | Default |
|---|---|---|---|
| tty | No | Allocate a pseudo-terminal | |
| command | Yes | Shell command to execute | |
| profile | No | Profile name | |
| session | No | Run in an existing interactive session (stateful) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already mark destructiveHint=true, and the description builds on that by adding 'May modify the system' and describing approval-gate behavior based on approvalPolicy. This provides useful safety context beyond the structured annotation without contradicting it. It stops short of detailing what refusal or prompt looks like, but it is still solid.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, front-loaded with the main action and progressively adding risk and policy context. No filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a mutation tool marked destructive with a 'session' parameter and several close siblings, the description covers risk and approval policy but omits how command output is returned, whether execution is synchronous, and how this relates to privileged-command/open-session. That leaves meaningful gaps despite the good core.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema covers all 4 parameters (100% coverage), so the baseline is 3. The description adds marginal semantics by linking the profile parameter to approval-policy decisions, but it does not clarify tty or session beyond the schema descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening sentence names a specific verb and resource: 'Execute an arbitrary shell command on the remote server.' This makes the core function unmistakable and distinguishes it from read-only/session-management siblings, though it does not explicitly contrast with privileged-command.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No when-to-use guidance is given. The description explains the approval gate for destructive/privileged commands but does not say when to choose run-command over privileged-command, read-command, or open-session, nor when not to use it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
sftp-downloadARead-only
Download a file from the remote server via SFTP.
| Name | Required | Description | Default |
|---|---|---|---|
| profile | No | Profile name | |
| remotePath | Yes | Remote file path to download |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already supply readOnlyHint=true, so the safety profile is carried by structured data. The description adds only the protocol detail ('via SFTP') and does not disclose practical behavior such as where the file is saved locally or whether an existing session/profile is required.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single front-loaded sentence with no filler. Every word contributes to identifying the action, target, and protocol.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple read-only download with two parameters, the description is minimally adequate, but it omits practical context such as expected outcome, local destination, or prerequisites. Since there is no output schema, a sentence on the result would have rounded out the picture.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the parameters remotePath and profile are already documented in the input schema. The description adds no additional parameter-level meaning beyond confirming that the download source is a remote path.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a specific verb ('Download'), a resource ('a file from the remote server'), and the protocol ('via SFTP'), which clearly distinguishes it from siblings such as sftp-upload. No ambiguity about the tool's primary function.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The intended use is implied by the verb 'Download', and the sibling set contains sftp-upload as an obvious alternative, but the description does not state when to prefer this tool, prerequisites such as an open session or profile, or when not to use it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
sftp-uploadADestructive
Upload a file to the remote server via SFTP (secure file transfer, not shell-based).
| Name | Required | Description | Default |
|---|---|---|---|
| content | Yes | File content to upload | |
| profile | No | Profile name | |
| remotePath | Yes | Remote file path |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The destructiveHint annotation already signals mutating behavior, so the bar is lower. The description adds that the transfer is SFTP not shell, but it does not disclose whether an existing remote file is overwritten or whether an active session/profile is required, which would be valuable behavioral context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with no filler. It conveys the action, destination, transport protocol, and an explicit distinction from shell-based tools.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The definition is adequate for a straightforward upload, but it omits overwrite behavior and any session/profile prerequisite even though sibling session tools exist and destructiveHint is true. An agent could invoke it without knowing whether the remote file will be replaced.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents remotePath, content, and profile. The description adds no parameter-level detail beyond the schema, justifying the baseline score.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a concrete action and resource: 'Upload a file to the remote server via SFTP'. The parenthetical 'secure file transfer, not shell-based' distinguishes it from the shell-based sibling commands and implies the opposite of sftp-download.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The 'not shell-based' clause gives an implicit exclusion, but the description never states when to choose this over siblings such as sftp-download or run-command, or prerequisites like an open session/profile. Usage context is implied by the verb rather than explicitly guided.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
signal-processADestructive
Send a signal (INT, TERM, KILL) to a remote process by PID.
| Name | Required | Description | Default |
|---|---|---|---|
| pid | Yes | Process ID to signal (positive integer) | |
| signal | No | Signal to send | TERM |
| profile | No | Profile name |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The destructiveHint annotation already signals that this is a destructive operation. The description adds that the target is a remote process and enumerates the available signals, but it does not disclose consequences such as process termination, irreversibility, or that KILL is more forceful than TERM. No contradiction with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single, front-loaded sentence with no filler. It states the action, signal options, target, and identifier in minimal words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple 3-parameter mutation with a destructive annotation, this is mostly adequate. However, it lacks operational details: what the profile is for, whether an active session is required, and what a successful call returns (no output schema). These are not fatal for a simple tool, but they are clear gaps.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3. The description adds little beyond the schema: it mentions PID and the signal enum, both already defined. The 'profile' parameter remains unexplained in the description, though the schema labels it 'Profile name'.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb ('send'), a precise object ('signal'), and a clear target ('remote process by PID'). It also lists the supported signal values (INT, TERM, KILL), making the operation unambiguous and naturally distinct from sibling session/command/SFTP tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no statement about when to choose this tool, what prerequisites are required (e.g., an open session/connection or profile), or when not to use it. The purpose implies the use case, but no guidance is provided and sibling tools like run-command are not ruled out.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
11 tool updates
v2.5.0- First observed
close-session - First observed
list-connections - First observed
list-sessions - First observed
open-session - First observed
privileged-command - First observed
read-command - First observed
read-session-output - First observed
run-command - First observed
sftp-download - First observed
sftp-upload - First observed
signal-process
TDQS
Each tool maps to a distinct action/resource: connection discovery, session lifecycle, command types (read-only vs arbitrary vs sudo), process signals, and SFTP transfers. The overlapping command-execution tools are clearly separated by side-effect level and approval requirements.
All names use the same lowercase kebab-case style and nearly all follow a verb-object pattern (list-, open-, close-, read-, run-, signal-). The sftp- prefix is consistent for the transfer pair.
11 tools is well within the ideal range for an SSH server; each tool covers a distinct need (discovery, sessions, command execution, signaling, file transfer) without unnecessary sprawl.
The server covers connection discovery, session lifecycle, read/run/sudo command execution, process signaling, and SFTP file transfer. The only minor gap is that interactive sessions lack an explicit send-command tool, though agents can likely work around it with run-command.
Maintenance
Related MCP Connectors
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
The Remote MCP server acts as a standardized bridge between LLM applications (like Claude, ChatGPT, and Cursor) and external services, enabling AI agents to access external tools and resources. Its primary capability is providing a centralized search tool to discover other MCP servers and their respective tools. Unlike local implementations, it runs remotely with OAuth authentication and permission controls for security.
Enable secure connectivity between Sentry issues and debugging data, and LLM clients, using a Model Context Protocol (MCP) server.
Related MCP Servers
- AlicenseBqualityDmaintenanceA server that enables remote command execution over SSH through the Model Context Protocol (MCP), supporting both password and private key authentication.112MIT
- FlicenseAqualityDmaintenanceA local Model Context Protocol server that allows LLMs to securely execute shell commands on remote Linux and Windows systems via SSH connections.6172-
- AlicenseAqualityDmaintenanceA Model Context Protocol server for secure local system operations, enabling shell command execution and file management via a standardized interface.141Apache 2.0
- AlicenseNot gradedqualityAmaintenanceA local MCP server that enables LLMs to execute shell commands on remote hosts over SSH with multiple authentication methods.353MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/tufantunc/ssh-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server