using System.ComponentModel; using McpSsh.Server.Sftp; using McpSsh.Server.Ssh; using McpSsh.Server.Terminal; using ModelContextProtocol.Server; namespace McpSsh.Server.Tools; [McpServerToolType] public sealed class SshTools(SshExecService sshExecService, TerminalSessionManager terminalSessionManager, SftpService sftpService) { private readonly SshExecService _sshExecService = sshExecService; private readonly TerminalSessionManager _terminalSessionManager = terminalSessionManager; private readonly SftpService _sftpService = sftpService; [McpServerTool(Name = "ssh_exec", Destructive = false)] [Description("Execute a single command over SSH using key-based authentication.")] public Task ExecuteAsync( [Description("Remote hostname or IP address. OpenSSH aliases are not supported in this vertical slice.")] string host, [Description("Remote SSH username.")] string username, [Description("Command to execute on the remote host.")] string command, [Description("Remote SSH port. Defaults to 22.")] int? port = null, [Description("Optional remote working directory.")] string? cwd = null, [Description("Optional local private key path. Defaults to ~/.ssh/id_ed25519, ~/.ssh/id_ecdsa, then ~/.ssh/id_rsa.")] string? keyPath = null, [Description("Optional private key passphrase. Use only with trusted MCP clients.")] string? keyPassphrase = null, [Description("Timeout in seconds. Defaults to 30 and is capped at 300.")] int? timeoutSeconds = null, CancellationToken cancellationToken = default) { return _sshExecService.ExecuteAsync(host, username, command, port, cwd, keyPath, keyPassphrase, timeoutSeconds, cancellationToken); } [McpServerTool(Name = "ssh_terminal_start", Destructive = false)] [Description("Start a persistent SSH PTY shell session using key-based authentication.")] public Task StartTerminalAsync( [Description("Remote hostname or IP address. OpenSSH aliases are not supported in this vertical slice.")] string host, [Description("Remote SSH username.")] string username, [Description("Terminal columns. Defaults to 120.")] int? cols = null, [Description("Terminal rows. Defaults to 40.")] int? rows = null, [Description("Remote SSH port. Defaults to 22.")] int? port = null, [Description("Optional local private key path. Defaults to ~/.ssh/id_ed25519, ~/.ssh/id_ecdsa, then ~/.ssh/id_rsa.")] string? keyPath = null, [Description("Optional private key passphrase. Use only with trusted MCP clients.")] string? keyPassphrase = null, [Description("Idle timeout in seconds. Defaults to 900.")] int? idleTimeoutSeconds = null, CancellationToken cancellationToken = default) { return _terminalSessionManager.StartAsync(host, username, cols, rows, port, keyPath, keyPassphrase, idleTimeoutSeconds, cancellationToken); } [McpServerTool(Name = "ssh_terminal_write", Destructive = false)] [Description("Write input to an active SSH terminal session.")] public TerminalWriteResult WriteTerminal( [Description("Terminal session ID returned by terminal_start.")] string sessionId, [Description("Input to write to the terminal. Include newline characters when submitting commands.")] string input) { return _terminalSessionManager.Write(sessionId, input); } [McpServerTool(Name = "ssh_terminal_read", Destructive = false)] [Description("Read buffered output from an active SSH terminal session.")] public TerminalReadResult ReadTerminal( [Description("Terminal session ID returned by terminal_start.")] string sessionId, [Description("Maximum output characters to read. Defaults to 12000.")] int? maxBytes = null) { return _terminalSessionManager.Read(sessionId, maxBytes); } [McpServerTool(Name = "ssh_terminal_stop", Destructive = false)] [Description("Stop and remove an SSH terminal session.")] public TerminalStopResult StopTerminal( [Description("Terminal session ID returned by terminal_start.")] string sessionId) { return _terminalSessionManager.Stop(sessionId); } [McpServerTool(Name = "sftp_list", Destructive = false)] [Description("List remote directory contents over SFTP.")] public Task ListSftpAsync( [Description("Remote hostname or IP address. OpenSSH aliases are not supported in this vertical slice.")] string host, [Description("Remote SSH username.")] string username, [Description("Remote directory path to list.")] string remotePath, [Description("Remote SSH port. Defaults to 22.")] int? port = null, [Description("Optional local private key path. Defaults to ~/.ssh/id_ed25519, ~/.ssh/id_ecdsa, then ~/.ssh/id_rsa.")] string? keyPath = null, [Description("Optional private key passphrase. Use only with trusted MCP clients.")] string? keyPassphrase = null, CancellationToken cancellationToken = default) { return _sftpService.ListAsync(host, username, remotePath, port, keyPath, keyPassphrase, cancellationToken); } [McpServerTool(Name = "sftp_get", Destructive = false)] [Description("Download a remote file using SFTP, silently falling back to SCP when SFTP is unavailable.")] public Task GetSftpAsync( [Description("Remote hostname or IP address. OpenSSH aliases are not supported in this vertical slice.")] string host, [Description("Remote SSH username.")] string username, [Description("Remote file path to download.")] string remotePath, [Description("Local destination path. Must stay within the server working directory.")] string localPath, [Description("Remote SSH port. Defaults to 22.")] int? port = null, [Description("Optional local private key path. Defaults to ~/.ssh/id_ed25519, ~/.ssh/id_ecdsa, then ~/.ssh/id_rsa.")] string? keyPath = null, [Description("Optional private key passphrase. Use only with trusted MCP clients.")] string? keyPassphrase = null, [Description("Overwrite an existing local file. Defaults to false.")] bool? overwrite = null, [Description("Maximum download size in bytes. Defaults to 104857600.")] long? maxBytes = null, CancellationToken cancellationToken = default) { return _sftpService.GetAsync(host, username, remotePath, localPath, port, keyPath, keyPassphrase, overwrite, maxBytes, cancellationToken); } [McpServerTool(Name = "sftp_put", Destructive = false)] [Description("Upload a local file using SFTP, silently falling back to SCP when SFTP is unavailable.")] public Task PutSftpAsync( [Description("Remote hostname or IP address. OpenSSH aliases are not supported in this vertical slice.")] string host, [Description("Remote SSH username.")] string username, [Description("Local file path to upload. Must stay within the server working directory.")] string localPath, [Description("Remote destination file path.")] string remotePath, [Description("Remote SSH port. Defaults to 22.")] int? port = null, [Description("Optional local private key path. Defaults to ~/.ssh/id_ed25519, ~/.ssh/id_ecdsa, then ~/.ssh/id_rsa.")] string? keyPath = null, [Description("Optional private key passphrase. Use only with trusted MCP clients.")] string? keyPassphrase = null, [Description("Overwrite an existing remote file. Defaults to false.")] bool? overwrite = null, [Description("Maximum upload size in bytes. Defaults to 104857600.")] long? maxBytes = null, CancellationToken cancellationToken = default) { return _sftpService.PutAsync(host, username, localPath, remotePath, port, keyPath, keyPassphrase, overwrite, maxBytes, cancellationToken); } }