mcp_ssh_connectors
A host-side MCP server and local terminal tool that let an AI use the host machine's OpenSSH client to reach any SSH server with a request-scoped password or private key, even when the model sandbox has no direct network path. Configured named targets remain available for compatibility.
通过宿主机的 OpenSSH 客户端,让 AI 使用请求中提供的密码或私钥连接任意 SSH 服务器;支持本地 stdio,也支持带 API Key 鉴权的 Streamable HTTP MCP。
Dynamic MCP connections can execute any validated single-line command. HTTP clients still need a scoped, unexpired Bearer API key. Credentials are request-scoped and are never logged or returned.
Architecture
flowchart LR
AI[AI / MCP client] -->|stdio, local OS boundary| S[mcp-ssh-server]
AI -->|HTTP + Bearer API key| H[mcp-ssh-http]
H --> K[scrypt key store + scopes]
S --> P[dynamic connection or configured target]
H --> P
U[Human terminal] -->|mcp-ssh CLI| P
P -->|spawn, shell=false| O[Host OpenSSH client]
O --> R[configured SSH instances]
P --> A[JSONL audit log]
Node.js 20+ and an OpenSSH-compatible ssh executable are required.
Included
- MCP tools:
ssh_list_targets,ssh_preview,ssh_check,ssh_exec; check/exec accept configured targets or arbitrary dynamic connections - Local stdio MCP and authenticated Streamable HTTP MCP
- API key creation, listing, expiry, target scopes, operation scopes, and revocation
- Local CLI:
init,targets,preview,check,exec,connect,mcp,http,key - TOFU host-key checking for dynamic servers, request-scoped credentials, timeouts, output caps, and JSONL auditing
- ProxyJump, identity-file, port, and dedicated known-hosts support
- CI and unit tests
Install
git clone https://github.com/xtawa/mcp_ssh_connectors.git
cd mcp_ssh_connectors
npm install
npm run check
npm link
mcp-ssh init
$EDITOR ~/.config/mcp-ssh/config.json
After adding an optional configured target, it can still be tested through the CLI:
ssh example
mcp-ssh preview example -- uname -a
mcp-ssh check example
mcp-ssh exec example -- uname -a
Dynamic SSH connections
ssh_check and ssh_exec accept a connection object instead of a configured target. The connection requires a host, username, optional port, and exactly one authentication method.
Password example:
{
"connection": {
"host": "203.0.113.10",
"username": "deploy",
"port": 22,
"authentication": {
"type": "password",
"password": "request-scoped-password"
}
},
"command": "uname -a",
"reason": "diagnostics"
}
Private-key example:
{
"connection": {
"host": "server.example.com",
"username": "root",
"authentication": {
"type": "privateKey",
"privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----",
"passphrase": "optional-key-passphrase"
}
},
"command": "systemctl status example"
}
Dynamic commands are not checked against configured allow/deny expressions. They must be non-empty, single-line, NUL-free, and within policy.maxCommandLength. Dynamic calls use -F none, so local/system SSH configuration cannot inject a proxy or other options. New host keys use StrictHostKeyChecking=accept-new: the first key is recorded, while later key changes are rejected.
Passwords and key passphrases are supplied to OpenSSH through a forced SSH_ASKPASS helper. Private-key content is written to a per-request 0600 temporary file; Windows also receives an explicit user-only ACL. Temporary credential files are removed when the request finishes. Credentials are not included in process arguments, audit records, tool results, or error objects.
Local stdio MCP
{
"mcpServers": {
"ssh-connectors": {
"command": "mcp-ssh-server",
"env": { "MCP_SSH_CONFIG": "/absolute/path/to/config.json" }
}
}
}
Stdio relies on the local OS account boundary; no API key is sent through the model context.
HTTP MCP with API Key authentication
Create a read-only key restricted to staging:
mcp-ssh key create staging-observer \
--scopes mcp,ssh:read \
--expires 30d
Create an execution key for two targets:
mcp-ssh key create deploy-agent \
--scopes mcp,ssh:read,ssh:exec \
--expires 7d
The complete token is shown once. The key store contains only its salted scrypt hash. List metadata or revoke a key:
mcp-ssh key list
mcp-ssh key revoke KEY_ID
Start the authenticated endpoint:
mcp-ssh http
# or: mcp-ssh-http
# http://127.0.0.1:3000/mcp
Clients send:
Authorization: Bearer mcp_ssh.KEY_ID.SECRET
Example MCP client configuration:
{
"mcpServers": {
"ssh-connectors-http": {
"url": "http://127.0.0.1:3000/mcp",
"headers": { "Authorization": "Bearer ${MCP_SSH_API_KEY}" }
}
}
}
Keep the token in the client environment or secret manager. Do not commit it. For any non-loopback deployment, use TLS and set http.allowedHosts; preferably place the service behind a hardened reverse proxy.
Scopes
| Scope | Permission |
|---|---|
mcp |
Required to reach the MCP endpoint |
ssh:read |
List targets, preview commands, and check connectivity |
ssh:exec |
Execute commands that also pass host policy |
New API keys default to target *, which permits their SSH operation scopes to use dynamic connections. --targets remains available to restrict access to legacy configured targets; it does not restrict dynamic hosts.
Configuration
{
"version": 1,
"sshBinary": "ssh",
"auth": { "keyStore": "~/.config/mcp-ssh/keys.json" },
"http": {
"host": "127.0.0.1",
"port": 3000,
"allowedHosts": [],
"allowedOrigins": []
},
"audit": { "required": true, "logCommands": false },
"defaults": {
"timeoutMs": 30000,
"connectTimeoutSeconds": 10,
"maxOutputBytes": 1048576,
"knownHostsFile": "~/.ssh/known_hosts"
},
"policy": {
"maxCommandLength": 4096,
"deniedCommands": ["(?:^|\\s)sudo(?:\\s|$)"]
},
"targets": {
"staging": {
"destination": "[email protected]",
"identityFile": "~/.ssh/staging_ed25519",
"proxyJump": "bastion",
"tags": ["staging", "linux"],
"allowedCommands": [
"^uname -a$",
"^systemctl status [A-Za-z0-9_.@-]+$"
],
"deniedCommands": ["(?:^|[;&|]\\s*)rm(?:\\s|$)"],
"requireReason": true
}
}
}
The targets object is optional and may be empty. For configured targets, deny rules run before allow rules and a target with no allowedCommands is blocked. Dynamic connections bypass these configured command expressions and retain only the global length and single-line validation.
Terminal commands
mcp-ssh init [--config PATH]
mcp-ssh targets [--config PATH]
mcp-ssh preview TARGET [--reason TEXT] -- COMMAND
mcp-ssh check TARGET [--config PATH]
mcp-ssh exec TARGET [--reason TEXT] -- COMMAND
mcp-ssh connect TARGET [--config PATH]
mcp-ssh mcp [--config PATH]
mcp-ssh http [--host HOST] [--port PORT] [--config PATH]
mcp-ssh key create NAME [--targets LIST] [--scopes LIST] [--expires 30d]
mcp-ssh key list [--config PATH]
mcp-ssh key revoke KEY_ID [--config PATH]
connect is a human-only interactive shell and is not exposed as an MCP tool.
Request authorization order
For HTTP requests the connector applies four independent checks:
- validate the Bearer key hash, expiry, and revocation state;
- require operation scope (
ssh:readorssh:exec), plus target access only for a configured target; - validate dynamic single-line commands, or apply configured-target deny/allow rules;
- authenticate to the remote machine with the request password/private key or configured host SSH identity.
The API key id is recorded as the audit actor. Neither bearer tokens nor SSH key contents are logged.
Security
Read docs/security.md before exposing this server. Dynamic access deliberately gives an ssh:exec caller broad reach. Plain HTTP should stay on loopback or inside a trusted tunnel. Non-loopback deployments need TLS, explicit host/origin policy, short-lived scoped keys, and append-only audit storage.
Ideas and next steps
See docs/roadmap.md for human approvals, external identity providers, ephemeral SSH certificates, constrained SFTP, fleet blast-radius budgets, cached host facts, telemetry, and session recording.