9.4 KiB
AGENTS.md
Project
MCP SSH Server
Self-contained MCP server binary that provides SSH command execution, persistent terminal sessions, and file transfer support with minimal user configuration.
The implementation must rely on the user's existing OpenSSH configuration and SSH environment.
Core Product Requirements
Long-Term Goals
- SSH command execution
- Persistent interactive terminal sessions
- Remote file transfer support
- Minimal user-side MCP configuration
- Self-contained binary distribution
- Use existing
~/.ssh/config - Use existing SSH keys and SSH agent
- Support SSH aliases exactly as users use them in terminal
Current Vertical Slice
The first implementation pass intentionally narrows the MVP:
- .NET 10 stdio MCP server
ssh_execterminal_start,terminal_write,terminal_read, andterminal_stopsftp_list,sftp_get, andsftp_put- SSH.NET-based SSH connections
- Tool-supplied
host,username, optionalport, optionalkeyPath, and optionalkeyPassphrase - Default key discovery from
~/.ssh/id_ed25519,~/.ssh/id_ecdsa, then~/.ssh/id_rsawhenkeyPathis omitted - SFTP first for file transfer;
sftp_getandsftp_putsilently fall back to SCP if SFTP is unavailable - No SSH agent, OpenSSH alias,
ssh -G, ProxyJump, or ProxyCommand support yet - Basic audit logging and timeout enforcement
Non-Goals
- Do not replace OpenSSH
- Do not store SSH private keys
- Do not expose arbitrary local shell execution
- Do not require duplicate SSH configuration inside MCP settings
- Do not build a web UI for MVP
Distribution Requirements
The server must ship as a single self-contained executable.
Target platforms:
- Windows x64
- Linux x64
- Linux arm64
- macOS x64
- macOS arm64
Expected binary names:
mcp-ssh
mcp-ssh.exe
Example .NET publish command:
dotnet publish \
-c Release \
-r linux-x64 \
--self-contained true \
/p:PublishSingleFile=true \
/p:PublishTrimmed=true
MCP Transport
Use stdio transport for MVP.
Example MCP config:
{
"mcpServers": {
"ssh": {
"command": "/path/to/mcp-ssh",
"args": []
}
}
}
No SSH-specific configuration should be required in MCP config.
SSH Configuration Resolution
The long-term implementation should use OpenSSH configuration resolution.
Primary mechanism:
ssh -G <host-alias>
The implementation should parse the resolved output.
Required resolved fields:
- hostname
- user
- port
- identityfile
- proxyjump
- proxycommand
- stricthostkeychecking
- userknownhostsfile
- identitiesonly
The implementation must support:
- SSH aliases
- bastion hosts
- proxy jump
- existing SSH agent
- known hosts validation
Fallback parsing of ~/.ssh/config may be implemented if ssh -G is unavailable.
This is not part of the current vertical slice. The current implementation treats host as a hostname or IP address supplied directly to the MCP tool.
Authentication Requirements
Authentication should reuse the user's existing SSH environment.
Supported:
- SSH agent
- identity files
- OpenSSH config
- known hosts
- optional password prompt support
The implementation must never persist private keys.
File Transfer Strategy
SFTP is the primary transfer mechanism.
Rationale:
- More stable across SSH implementations
- Better metadata support
- Better directory traversal semantics
- Avoids SCP shell parsing issues
- Modern OpenSSH increasingly routes SCP over SFTP internally
Priority order:
1. SFTP
2. SCP fallback for get/put
The implementation must:
- Use SFTP for directory listing
- Use SFTP first for file get/put
- Silently fall back to SCP for file get/put if SFTP is unavailable
Example error:
{
"error": "sftp_unavailable",
"message": "Remote host does not expose the SFTP subsystem for directory listing.",
"scpFallbackAvailable": false
}
MCP Tools
ssh_exec
Execute a single SSH command.
Input:
{
"host": "prod-api.example.com",
"username": "deploy",
"command": "systemctl status nginx",
"cwd": "/var/www",
"port": 22,
"keyPath": "~/.ssh/id_ed25519",
"keyPassphrase": "optional-passphrase",
"timeoutSeconds": 30
}
Output:
{
"exitCode": 0,
"stdout": "...",
"stderr": "...",
"durationMs": 421
}
Requirements:
- Enforce timeout
- Capture stdout separately
- Capture stderr separately
- Preserve non-zero exit codes
- Authenticate with explicit
keyPath, or the first available default private key from~/.ssh/id_ed25519,~/.ssh/id_ecdsa, then~/.ssh/id_rsa - Support optional
keyPassphrasefor encrypted private keys
terminal_start
Start a persistent PTY shell session.
Input:
{
"host": "prod-api.example.com",
"username": "deploy",
"cols": 120,
"rows": 40,
"port": 22,
"keyPath": "~/.ssh/id_ed25519",
"keyPassphrase": "optional-passphrase",
"idleTimeoutSeconds": 900
}
Output:
{
"sessionId": "term_abc123"
}
Requirements:
- Allocate PTY
- Maintain server-side session state
- Support idle timeout cleanup
- Use the same key-auth inputs and default key discovery as
ssh_exec - Use the remote account's default shell; do not write shell setup commands into the PTY after startup
terminal_write
Write to an active terminal session.
Input:
{
"sessionId": "term_abc123",
"input": "tail -f /var/log/nginx/error.log\\n"
}
Output:
{
"accepted": true
}
terminal_read
Read buffered output from a terminal session.
Input:
{
"sessionId": "term_abc123",
"maxBytes": 12000
}
Output:
{
"output": "...",
"truncated": false
}
terminal_stop
Stop and remove a terminal session.
Input:
{
"sessionId": "term_abc123"
}
Output:
{
"stopped": true
}
sftp_list
List remote directory contents.
Input:
{
"host": "prod-api",
"remotePath": "/var/www"
}
Output:
{
"entries": [
{
"name": "app",
"path": "/var/www/app",
"type": "directory",
"size": 4096,
"modifiedUtc": "2026-05-24T12:00:00Z"
}
]
}
sftp_get
Download a remote file.
Input:
{
"host": "prod-api",
"remotePath": "/var/log/app.log",
"localPath": "./downloads/app.log"
}
Output:
{
"bytesTransferred": 123456,
"localPath": "./downloads/app.log"
}
Requirements:
- Enforce max download size
- Prevent unsafe local path traversal
- Fail on overwrite unless explicitly enabled
sftp_put
Upload a local file.
Input:
{
"host": "prod-api",
"localPath": "./dist/app.tar.gz",
"remotePath": "/tmp/app.tar.gz",
"overwrite": false
}
Output:
{
"bytesTransferred": 123456,
"remotePath": "/tmp/app.tar.gz"
}
Requirements:
- Enforce max upload size
- Fail if remote file exists and overwrite is false
Session Management
Terminal sessions should be maintained in memory.
Example session state:
{
"sessionId": "term_abc123",
"host": "prod-api",
"createdUtc": "...",
"lastActivityUtc": "...",
"idleTimeoutSeconds": 900
}
Requirements:
- Cryptographically random session IDs
- Idle timeout cleanup
- Graceful cleanup on shutdown
- Output buffering with maximum limits
Security Requirements
The implementation must default to safe behavior.
Required safeguards:
- Command timeout enforcement
- Upload size limits
- Download size limits
- Audit logging
- No private key persistence
- No arbitrary local command execution
- No command-content or host blocking; access control is delegated to SSH users, SSH keys, and remote-side permissions
Audit Logging
Every tool call should be logged.
Example:
{
"timestampUtc": "2026-05-24T12:00:00Z",
"tool": "ssh_exec",
"host": "prod-api",
"command": "systemctl status nginx",
"success": true,
"durationMs": 421
}
Sensitive values must be redacted.
Recommended Implementation Stack
Language:
- C#
- .NET 10+
Recommended libraries:
- Official MCP SDK
- SSH.NET
- OpenSSH
ssh -G
Recommended Internal Interfaces
public interface ISshConfigResolver
{
/// <summary>
/// Resolves an SSH host alias into effective connection settings.
/// </summary>
/// <param name="hostAlias">The SSH host alias.</param>
/// <returns>The resolved SSH configuration.</returns>
ResolvedSshConfig Resolve(string hostAlias);
}
public interface ISshSessionFactory
{
/// <summary>
/// Creates a connected SSH client.
/// </summary>
/// <param name="hostAlias">The SSH host alias.</param>
/// <returns>A connected SSH client.</returns>
SshClient CreateSshClient(string hostAlias);
/// <summary>
/// Creates a connected SFTP client.
/// </summary>
/// <param name="hostAlias">The SSH host alias.</param>
/// <returns>A connected SFTP client.</returns>
SftpClient CreateSftpClient(string hostAlias);
}
MVP Scope
The MVP must include:
- Self-contained binary
- stdio transport
- ssh_exec
- terminal_start
- terminal_write
- terminal_read
- terminal_stop
- sftp_list
- sftp_get
- sftp_put
- basic audit logging
Future Enhancements
Potential future work:
- Streamable HTTP transport
- SCP compatibility fallback
- Per-host policy configuration
- File checksum validation
- Directory upload/download
- Remote command templates
- Resource-based remote file browsing
- Multi-hop SSH validation
- Secret redaction improvements
- Per-tool and per-host authorization policy