# 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_exec` * `terminal_start`, `terminal_write`, `terminal_read`, and `terminal_stop` * `sftp_list`, `sftp_get`, and `sftp_put` * SSH.NET-based SSH connections * Tool-supplied `host`, `username`, optional `port`, optional `keyPath`, and optional `keyPassphrase` * Default key discovery from `~/.ssh/id_ed25519`, `~/.ssh/id_ecdsa`, then `~/.ssh/id_rsa` when `keyPath` is omitted * SFTP first for file transfer; `sftp_get` and `sftp_put` silently 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: ```text mcp-ssh mcp-ssh.exe ``` Example .NET publish command: ```bash 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: ```json { "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: ```bash ssh -G ``` 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: ```text 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: ```json { "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: ```json { "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: ```json { "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 `keyPassphrase` for encrypted private keys --- ## terminal_start Start a persistent PTY shell session. Input: ```json { "host": "prod-api.example.com", "username": "deploy", "cols": 120, "rows": 40, "port": 22, "keyPath": "~/.ssh/id_ed25519", "keyPassphrase": "optional-passphrase", "idleTimeoutSeconds": 900 } ``` Output: ```json { "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: ```json { "sessionId": "term_abc123", "input": "tail -f /var/log/nginx/error.log\\n" } ``` Output: ```json { "accepted": true } ``` --- ## terminal_read Read buffered output from a terminal session. Input: ```json { "sessionId": "term_abc123", "maxBytes": 12000 } ``` Output: ```json { "output": "...", "truncated": false } ``` --- ## terminal_stop Stop and remove a terminal session. Input: ```json { "sessionId": "term_abc123" } ``` Output: ```json { "stopped": true } ``` --- ## sftp_list List remote directory contents. Input: ```json { "host": "prod-api", "remotePath": "/var/www" } ``` Output: ```json { "entries": [ { "name": "app", "path": "/var/www/app", "type": "directory", "size": 4096, "modifiedUtc": "2026-05-24T12:00:00Z" } ] } ``` --- ## sftp_get Download a remote file. Input: ```json { "host": "prod-api", "remotePath": "/var/log/app.log", "localPath": "./downloads/app.log" } ``` Output: ```json { "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: ```json { "host": "prod-api", "localPath": "./dist/app.tar.gz", "remotePath": "/tmp/app.tar.gz", "overwrite": false } ``` Output: ```json { "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: ```json { "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: ```json { "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 ```csharp public interface ISshConfigResolver { /// /// Resolves an SSH host alias into effective connection settings. /// /// The SSH host alias. /// The resolved SSH configuration. ResolvedSshConfig Resolve(string hostAlias); } public interface ISshSessionFactory { /// /// Creates a connected SSH client. /// /// The SSH host alias. /// A connected SSH client. SshClient CreateSshClient(string hostAlias); /// /// Creates a connected SFTP client. /// /// The SSH host alias. /// A connected SFTP client. 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