579 lines
9.4 KiB
Markdown
579 lines
9.4 KiB
Markdown
# 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 <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:
|
|
|
|
```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
|
|
{
|
|
/// <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
|