| ο»Ώusing System; |
| using System.Collections.Generic; |
| using System.ComponentModel; |
| using System.Diagnostics; |
| using System.IO; |
| using System.IO.Pipes; |
| using System.Linq; |
| using System.Text; |
| using System.Text.Json; |
| using System.Threading; |
| using System.Threading.Tasks; |
| using Microsoft.Extensions.DependencyInjection; |
| using Microsoft.Extensions.Hosting; |
| using Microsoft.Extensions.Logging; |
| using ModelContextProtocol.Server; |
|
|
| namespace ShellMcp |
| { |
| public class Program |
| { |
| public static async Task Main(string[] args) |
| { |
| var mode = Environment.GetEnvironmentVariable("SHELL_MCP_MODE") ?? "safe"; |
| ShellExecutor.Initialize(mode); |
|
|
| var builder = Host.CreateApplicationBuilder(args); |
| |
| |
| |
| builder.Logging.ClearProviders(); |
| |
| |
| |
| builder.Services |
| .AddMcpServer() |
| .WithStdioServerTransport() |
| .WithToolsFromAssembly(); |
| |
| await builder.Build().RunAsync(); |
| } |
| } |
|
|
| |
| |
| |
|
|
| public static class ShellExecutor |
| { |
| private static string _currentDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); |
| private static string _mode = "safe"; |
| private static int _defaultTimeout = 30; |
|
|
| private static readonly HashSet<string> SafeCommands = new(StringComparer.OrdinalIgnoreCase) |
| { |
| "dir", "ls", "pwd", "cd", "tree", |
| "type", "cat", "head", "tail", "more", "less", "find", "findstr", "grep", "where", "which", |
| "echo", "date", "time", "whoami", "hostname", "ver", |
| "git status", "git log", "git diff", "git branch", "git remote", "git fetch", "git show", "git ls-files", "git stash list", |
| "dotnet build", "dotnet run", "dotnet test", "dotnet restore", "dotnet clean", "dotnet --version", "dotnet --list-sdks", |
| "npm install", "npm run", "npm test", "npm list", "npm --version", "npm ci", "npm audit", |
| "node --version", "yarn --version", "yarn install", "yarn build", "yarn test", |
| "cls", "clear", "help", "man", |
| }; |
|
|
| private static readonly HashSet<string> DangerousCommands = new(StringComparer.OrdinalIgnoreCase) |
| { |
| "del", "rm", "rmdir", "rd", "erase", |
| "move", "mv", "rename", "ren", |
| "copy", "cp", "xcopy", "robocopy", |
| "mkdir", "md", |
| "git push", "git pull", "git merge", "git rebase", "git reset", "git clean", "git checkout", "git commit", "git add", "git rm", "git stash", |
| "taskkill", "kill", "shutdown", "restart", |
| "npm install -g", "npm uninstall", "dotnet tool install", |
| }; |
|
|
| private static readonly HashSet<string> BlockedCommands = new(StringComparer.OrdinalIgnoreCase) |
| { |
| "format", "diskpart", "reg", "regedit", "net user", "net localgroup", |
| "powershell -enc", "cmd /c", "rm -rf /", "del /s /q c:\\", |
| }; |
|
|
| public static void Initialize(string mode) |
| { |
| _mode = mode.ToLowerInvariant(); |
| var startDir = Environment.GetEnvironmentVariable("SHELL_MCP_START_DIR"); |
| if (!string.IsNullOrEmpty(startDir) && Directory.Exists(startDir)) |
| _currentDirectory = startDir; |
|
|
| |
| var bridgePath = Environment.GetEnvironmentVariable("SSH_BRIDGE_PATH"); |
| if (!string.IsNullOrEmpty(bridgePath)) |
| SshBridgeClient.SetBridgePath(bridgePath); |
| } |
|
|
| public static string Mode => _mode; |
| public static string CurrentDirectory => _currentDirectory; |
|
|
| public static bool IsCommandAllowed(string command, out string reason) |
| { |
| reason = ""; |
| string cmdLower = command.ToLowerInvariant().Trim(); |
| string firstWord = cmdLower.Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? ""; |
|
|
| foreach (var blocked in BlockedCommands) |
| { |
| if (cmdLower.Contains(blocked.ToLowerInvariant())) |
| { |
| reason = $"Command '{blocked}' is blocked for safety"; |
| return false; |
| } |
| } |
|
|
| if (_mode == "safe") |
| { |
| bool isSafe = SafeCommands.Any(safe => |
| cmdLower.StartsWith(safe.ToLowerInvariant()) || |
| firstWord == safe.ToLowerInvariant().Split(' ')[0]); |
|
|
| if (!isSafe) |
| { |
| reason = $"Command '{firstWord}' is not in the safe list. Use shell_dangerous for this operation."; |
| return false; |
| } |
| } |
| else if (_mode == "dangerous") |
| { |
| bool isAllowed = SafeCommands.Any(safe => |
| cmdLower.StartsWith(safe.ToLowerInvariant()) || |
| firstWord == safe.ToLowerInvariant().Split(' ')[0]) || |
| DangerousCommands.Any(dangerous => |
| cmdLower.StartsWith(dangerous.ToLowerInvariant()) || |
| firstWord == dangerous.ToLowerInvariant().Split(' ')[0]); |
|
|
| if (!isAllowed) |
| { |
| reason = $"Command '{firstWord}' is not recognized."; |
| return false; |
| } |
| } |
|
|
| return true; |
| } |
|
|
| public static CommandResult Execute(string command, int? timeoutSeconds = null) |
| { |
| var result = new CommandResult { Command = command }; |
| int timeout = timeoutSeconds ?? _defaultTimeout; |
|
|
| try |
| { |
| if (command.Trim().StartsWith("cd ", StringComparison.OrdinalIgnoreCase) || |
| command.Trim().Equals("cd", StringComparison.OrdinalIgnoreCase)) |
| { |
| return HandleCd(command); |
| } |
|
|
| if (!IsCommandAllowed(command, out string reason)) |
| { |
| result.Success = false; |
| result.Stderr = reason; |
| result.ExitCode = -1; |
| return result; |
| } |
|
|
| var psi = new ProcessStartInfo |
| { |
| FileName = "cmd.exe", |
| Arguments = $"/c {command}", |
| WorkingDirectory = _currentDirectory, |
| UseShellExecute = false, |
| RedirectStandardOutput = true, |
| RedirectStandardError = true, |
| CreateNoWindow = true, |
| StandardOutputEncoding = Encoding.UTF8, |
| StandardErrorEncoding = Encoding.UTF8, |
| }; |
|
|
| using var process = new Process { StartInfo = psi }; |
| var stdout = new StringBuilder(); |
| var stderr = new StringBuilder(); |
|
|
| process.OutputDataReceived += (s, e) => { if (e.Data != null) stdout.AppendLine(e.Data); }; |
| process.ErrorDataReceived += (s, e) => { if (e.Data != null) stderr.AppendLine(e.Data); }; |
|
|
| var sw = Stopwatch.StartNew(); |
| process.Start(); |
| process.BeginOutputReadLine(); |
| process.BeginErrorReadLine(); |
|
|
| bool completed = process.WaitForExit(timeout * 1000); |
| sw.Stop(); |
|
|
| if (!completed) |
| { |
| try { process.Kill(entireProcessTree: true); } catch { } |
| result.Success = false; |
| result.Stderr = $"Command timed out after {timeout} seconds"; |
| result.ExitCode = -1; |
| result.TimedOut = true; |
| } |
| else |
| { |
| result.Success = process.ExitCode == 0; |
| result.ExitCode = process.ExitCode; |
| result.Stdout = stdout.ToString().TrimEnd(); |
| result.Stderr = stderr.ToString().TrimEnd(); |
| } |
|
|
| result.ExecutionTimeMs = sw.ElapsedMilliseconds; |
| result.WorkingDirectory = _currentDirectory; |
| } |
| catch (Exception ex) |
| { |
| result.Success = false; |
| result.Stderr = $"Execution error: {ex.Message}"; |
| result.ExitCode = -1; |
| } |
|
|
| return result; |
| } |
|
|
| private static CommandResult HandleCd(string command) |
| { |
| var result = new CommandResult { Command = command }; |
| var parts = command.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); |
| |
| if (parts.Length == 1) |
| { |
| result.Success = true; |
| result.Stdout = _currentDirectory; |
| result.WorkingDirectory = _currentDirectory; |
| return result; |
| } |
|
|
| string targetPath = parts[1].Trim().Trim('"'); |
| string newPath; |
|
|
| if (Path.IsPathRooted(targetPath)) |
| newPath = targetPath; |
| else if (targetPath == "..") |
| newPath = Path.GetDirectoryName(_currentDirectory) ?? _currentDirectory; |
| else if (targetPath == "~") |
| newPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); |
| else |
| newPath = Path.Combine(_currentDirectory, targetPath); |
|
|
| newPath = Path.GetFullPath(newPath); |
|
|
| if (Directory.Exists(newPath)) |
| { |
| _currentDirectory = newPath; |
| result.Success = true; |
| result.Stdout = _currentDirectory; |
| result.WorkingDirectory = _currentDirectory; |
| } |
| else |
| { |
| result.Success = false; |
| result.Stderr = $"Directory not found: {newPath}"; |
| result.ExitCode = 1; |
| result.WorkingDirectory = _currentDirectory; |
| } |
|
|
| return result; |
| } |
|
|
| public static string GetAllowedCommands() |
| { |
| var sb = new StringBuilder(); |
| sb.AppendLine($"Mode: {_mode}"); |
| sb.AppendLine(); |
| sb.AppendLine("Safe commands (always allowed):"); |
| foreach (var cmd in SafeCommands.OrderBy(c => c)) |
| sb.AppendLine($" - {cmd}"); |
|
|
| if (_mode == "dangerous") |
| { |
| sb.AppendLine(); |
| sb.AppendLine("Dangerous commands (available in this mode):"); |
| foreach (var cmd in DangerousCommands.OrderBy(c => c)) |
| sb.AppendLine($" - {cmd}"); |
| } |
|
|
| sb.AppendLine(); |
| sb.AppendLine("Blocked commands (never allowed):"); |
| foreach (var cmd in BlockedCommands.OrderBy(c => c)) |
| sb.AppendLine($" - {cmd}"); |
|
|
| return sb.ToString(); |
| } |
| } |
|
|
| public class CommandResult |
| { |
| public string Command { get; set; } = ""; |
| public bool Success { get; set; } |
| public string Stdout { get; set; } = ""; |
| public string Stderr { get; set; } = ""; |
| public int ExitCode { get; set; } |
| public long ExecutionTimeMs { get; set; } |
| public string WorkingDirectory { get; set; } = ""; |
| public bool TimedOut { get; set; } |
|
|
| public override string ToString() |
| { |
| var sb = new StringBuilder(); |
| if (!string.IsNullOrEmpty(Stdout)) |
| sb.AppendLine(Stdout); |
| if (!string.IsNullOrEmpty(Stderr)) |
| { |
| if (sb.Length > 0) sb.AppendLine(); |
| sb.AppendLine($"[stderr] {Stderr}"); |
| } |
| sb.AppendLine(); |
| sb.AppendLine($"[exit: {ExitCode}] [time: {ExecutionTimeMs}ms] [cwd: {WorkingDirectory}]"); |
| if (TimedOut) |
| sb.AppendLine("[TIMED OUT]"); |
| return sb.ToString(); |
| } |
| } |
|
|
| |
| |
| |
|
|
| public static class SshBridgeClient |
| { |
| private const int PORT = 52718; |
|
|
| private static string? _bridgePath; |
|
|
| public static void SetBridgePath(string path) |
| { |
| _bridgePath = path; |
| } |
|
|
| public static string GetStatus() |
| { |
| try |
| { |
| return SendCommand("__STATUS__"); |
| } |
| catch |
| { |
| return "DISCONNECTED"; |
| } |
| } |
|
|
| public static bool LaunchBridge() |
| { |
| if (string.IsNullOrEmpty(_bridgePath) || !System.IO.File.Exists(_bridgePath)) |
| return false; |
|
|
| try |
| { |
| System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo |
| { |
| FileName = _bridgePath, |
| UseShellExecute = true |
| }); |
| return true; |
| } |
| catch |
| { |
| return false; |
| } |
| } |
|
|
| public static bool IsBridgeRunning() |
| { |
| try |
| { |
| using var client = new System.Net.Sockets.TcpClient(); |
| client.Connect("127.0.0.1", PORT); |
| return true; |
| } |
| catch |
| { |
| return false; |
| } |
| } |
|
|
| public static string SendCommand(string command) |
| { |
| using var client = new System.Net.Sockets.TcpClient(); |
| client.Connect("127.0.0.1", PORT); |
|
|
| using var stream = client.GetStream(); |
| using var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true }; |
| using var reader = new StreamReader(stream, Encoding.UTF8); |
|
|
| writer.WriteLine(command); |
| var response = reader.ReadLine() ?? "No response"; |
| |
| return response.Replace("<<CRLF>>", "\r\n").Replace("<<LF>>", "\n").Replace("<<CR>>", "\r"); |
| } |
|
|
| public static string Prefill(string host, int port, string user, string? password = null) |
| { |
| var cmd = $"__PREFILL__:{host}:{port}:{user}"; |
| if (!string.IsNullOrEmpty(password)) |
| cmd += $":{password}"; |
| return SendCommand(cmd); |
| } |
|
|
| public static string TriggerConnect() |
| { |
| return SendCommand("__CONNECT__"); |
| } |
|
|
| public static string GetPenStatus() |
| { |
| try |
| { |
| return SendCommand("__PEN_STATUS__"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string PutPenDown() |
| { |
| try |
| { |
| return SendCommand("__PEN_DOWN__"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string SendAbort() |
| { |
| try |
| { |
| return SendCommand("__ABORT__"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string IsRunning() |
| { |
| try |
| { |
| return SendCommand("__IS_RUNNING__"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string KillPort(int port) |
| { |
| try |
| { |
| return SendCommand($"__KILL_PORT__:{port}"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string SetTimeout(int seconds) |
| { |
| try |
| { |
| return SendCommand($"__TIMEOUT__:{seconds}"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string GetTail() |
| { |
| try |
| { |
| return SendCommand("__TAIL__"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string ListSpawned() |
| { |
| try |
| { |
| return SendCommand("__LIST_SPAWNED__"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string Spawn(string name, string command) |
| { |
| try |
| { |
| return SendCommand($"__SPAWN__:{name}:{command}"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string KillSpawned(string name) |
| { |
| try |
| { |
| return SendCommand($"__KILL_SPAWNED__:{name}"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string WriteFile(string path, string content) |
| { |
| try |
| { |
| |
| var encoded = content |
| .Replace("\r\n", "<<CRLF>>") |
| .Replace("\n", "<<LF>>") |
| .Replace("\r", "<<CR>>"); |
| return SendCommand($"__WRITE_FILE__:{path}|{encoded}"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
|
|
| public static string AppendFile(string path, string content) |
| { |
| try |
| { |
| var encoded = content |
| .Replace("\r\n", "<<CRLF>>") |
| .Replace("\n", "<<LF>>") |
| .Replace("\r", "<<CR>>"); |
| return SendCommand($"__APPEND_FILE__:{path}|{encoded}"); |
| } |
| catch |
| { |
| return "BRIDGE_NOT_RUNNING"; |
| } |
| } |
| } |
|
|
| |
| |
| |
|
|
| [McpServerToolType] |
| public static class ShellTools |
| { |
| [McpServerTool, Description("Execute a shell command. Working directory persists across calls. Use 'cd' to navigate. Check 'shell_info' for allowed commands.")] |
| public static string Shell( |
| [Description("Command to execute")] string command, |
| [Description("Timeout in seconds (default: 30)")] int? timeout = null) |
| { |
| if (string.IsNullOrWhiteSpace(command)) |
| return "Error: No command provided"; |
|
|
| var result = ShellExecutor.Execute(command, timeout); |
| return result.Success ? result.ToString() : $"β Command failed:\n{result}"; |
| } |
|
|
| [McpServerTool, Description("Get current working directory")] |
| public static string Pwd() |
| { |
| return ShellExecutor.CurrentDirectory; |
| } |
|
|
| [McpServerTool, Description("Show shell mode and list of allowed commands")] |
| public static string ShellInfo() |
| { |
| var sb = new StringBuilder(); |
| sb.AppendLine($"Shell MCP - {ShellExecutor.Mode.ToUpperInvariant()} mode"); |
| sb.AppendLine($"Current directory: {ShellExecutor.CurrentDirectory}"); |
| sb.AppendLine(); |
| sb.AppendLine(ShellExecutor.GetAllowedCommands()); |
| return sb.ToString(); |
| } |
|
|
| [McpServerTool, Description("Execute multiple commands in sequence. Stops on first failure unless continue_on_error is true.")] |
| public static string ShellBatch( |
| [Description("JSON array of commands: [\"cmd1\", \"cmd2\", ...]")] string commands_json, |
| [Description("Continue executing even if a command fails")] bool continue_on_error = false, |
| [Description("Timeout per command in seconds")] int? timeout = null) |
| { |
| try |
| { |
| var commands = JsonSerializer.Deserialize<List<string>>(commands_json); |
| if (commands == null || commands.Count == 0) |
| return "Error: No commands provided"; |
|
|
| var sb = new StringBuilder(); |
| int succeeded = 0, failed = 0; |
|
|
| foreach (var cmd in commands) |
| { |
| sb.AppendLine($"$ {cmd}"); |
| var result = ShellExecutor.Execute(cmd, timeout); |
| sb.AppendLine(result.ToString()); |
|
|
| if (result.Success) |
| succeeded++; |
| else |
| { |
| failed++; |
| if (!continue_on_error) |
| { |
| sb.AppendLine($"β Batch stopped. {succeeded} succeeded, {failed} failed, {commands.Count - succeeded - failed} skipped."); |
| return sb.ToString(); |
| } |
| } |
| } |
|
|
| sb.AppendLine($"β
Batch complete: {succeeded} succeeded, {failed} failed"); |
| return sb.ToString(); |
| } |
| catch (Exception ex) |
| { |
| return $"Error parsing commands: {ex.Message}"; |
| } |
| } |
|
|
| [McpServerTool, Description("Execute a command on a remote server via SSH Bridge. Requires SSH Bridge app to be running and connected.")] |
| public static string SshCommand( |
| [Description("Command to execute on the remote server")] string command) |
| { |
| if (string.IsNullOrWhiteSpace(command)) |
| return "Error: No command provided"; |
|
|
| try |
| { |
| |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| if (SshBridgeClient.LaunchBridge()) |
| { |
| return "π SSH Bridge launched!\n\nPlease:\n1. Enter host, user, and password in the window that just opened\n2. Click Connect\n3. Try this command again"; |
| } |
| else |
| { |
| return "β SSH Bridge not running and could not auto-launch.\n\nPlease run ssh-bridge.exe manually, or set SSH_BRIDGE_PATH in your Claude config."; |
| } |
| } |
|
|
| var status = SshBridgeClient.GetStatus(); |
| if (status == "DISCONNECTED") |
| { |
| return "β οΈ SSH Bridge is open but not connected.\n\nPlease:\n1. Enter host, user, and password\n2. Click Connect\n3. Try this command again"; |
| } |
|
|
| var result = SshBridgeClient.SendCommand(command); |
| return $"π‘ {status}\n\n{result}"; |
| } |
| catch (Exception ex) |
| { |
| return $"β SSH error: {ex.Message}"; |
| } |
| } |
|
|
| [McpServerTool, Description("Check if SSH Bridge is connected")] |
| public static string SshStatus() |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var status = SshBridgeClient.GetStatus(); |
| if (status.StartsWith("CONNECTED:")) |
| { |
| return $"β
{status.Replace("CONNECTED:", "Connected to ")}"; |
| } |
| return "β οΈ SSH Bridge open but not connected"; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Pre-fill SSH Bridge connection details. Launches SSH Bridge if not running, fills in host/port/user, optionally password. User must click Connect or you can call with auto_connect=true.")] |
| public static string SshPrefill( |
| [Description("SSH host/IP address")] string host, |
| [Description("SSH username")] string user, |
| [Description("SSH port (default: 22)")] int port = 22, |
| [Description("SSH password (optional - user can enter manually for security)")] string? password = null, |
| [Description("Automatically click Connect after prefilling")] bool auto_connect = false) |
| { |
| try |
| { |
| |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| if (!SshBridgeClient.LaunchBridge()) |
| { |
| return "β Could not launch SSH Bridge. Set SSH_BRIDGE_PATH in your Claude config."; |
| } |
| |
| Thread.Sleep(1000); |
| } |
|
|
| |
| var result = SshBridgeClient.Prefill(host, port, user, password); |
| if (result != "PREFILLED") |
| { |
| return $"β Prefill failed: {result}"; |
| } |
|
|
| if (auto_connect && !string.IsNullOrEmpty(password)) |
| { |
| Thread.Sleep(200); |
| SshBridgeClient.TriggerConnect(); |
| Thread.Sleep(2000); |
| var status = SshBridgeClient.GetStatus(); |
| if (status.StartsWith("CONNECTED:")) |
| { |
| return $"β
Connected to {user}@{host}:{port}"; |
| } |
| return $"β οΈ Connection initiated. Status: {status}"; |
| } |
|
|
| if (string.IsNullOrEmpty(password)) |
| { |
| return $"π SSH Bridge prefilled with {user}@{host}:{port}\n\nPlease enter password and click Connect."; |
| } |
| return $"π SSH Bridge prefilled with {user}@{host}:{port}\n\nClick Connect when ready."; |
| } |
| catch (Exception ex) |
| { |
| return $"β Error: {ex.Message}"; |
| } |
| } |
|
|
| [McpServerTool, Description("Check if user has lifted the pen (paused command execution). When pen is lifted, commands will be blocked until pen is put back down.")] |
| public static string SshPenStatus() |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var status = SshBridgeClient.GetPenStatus(); |
| return status switch |
| { |
| "PEN_LIFTED" => "β Pen is LIFTED - User has paused command execution. Wait for user or call SshPenDown to request resumption.", |
| "PEN_DOWN" => "βοΈ Pen is DOWN - Commands will execute normally.", |
| _ => $"β οΈ Unknown status: {status}" |
| }; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Request to put the pen back down (resume command execution). Only works if pen was lifted. User can also click the button manually.")] |
| public static string SshPenDown() |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var result = SshBridgeClient.PutPenDown(); |
| return result switch |
| { |
| "PEN_LOWERED" => "βοΈ Pen lowered - Command execution resumed.", |
| "PEN_ALREADY_DOWN" => "βοΈ Pen was already down - Commands executing normally.", |
| _ => $"β οΈ Unexpected response: {result}" |
| }; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Send Ctrl+C (abort signal) to the currently running command. Use this when a command is taking too long or appears stuck.")] |
| public static string SshAbort() |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var result = SshBridgeClient.SendAbort(); |
| return result switch |
| { |
| "ABORT_SENT" => "π Abort signal (Ctrl+C) sent to running command.", |
| "NO_COMMAND_RUNNING" => "βΉοΈ No command currently running.", |
| _ => $"β οΈ Response: {result}" |
| }; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Check if a command is currently running on the SSH connection.")] |
| public static string SshIsRunning() |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var result = SshBridgeClient.IsRunning(); |
| return result switch |
| { |
| "RUNNING" => "β‘ A command is currently running.", |
| "IDLE" => "β
No command running - ready for new commands.", |
| _ => $"β οΈ Status: {result}" |
| }; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Kill process listening on a specific port. Useful for freeing up ports or killing hung services.")] |
| public static string SshKillPort( |
| [Description("Port number to kill (1-65535)")] int port) |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| if (port < 1 || port > 65535) |
| { |
| return "β Invalid port number. Must be 1-65535."; |
| } |
|
|
| var result = SshBridgeClient.KillPort(port); |
| return $"π« Kill port {port}:\n{result}"; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Set timeout for the next SSH command. Useful for long-running operations like large downloads.")] |
| public static string SshSetTimeout( |
| [Description("Timeout in seconds (1-3600)")] int seconds) |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| if (seconds < 1 || seconds > 3600) |
| { |
| return "β Invalid timeout. Must be 1-3600 seconds."; |
| } |
|
|
| var result = SshBridgeClient.SetTimeout(seconds); |
| return $"β±οΈ {result}"; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Get the last 50 lines of output from the SSH Bridge terminal. Useful for checking progress of background tasks.")] |
| public static string SshTail() |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var result = SshBridgeClient.GetTail(); |
| return $"π Recent output:\n{result}"; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("List all tracked background processes spawned via SshSpawn.")] |
| public static string SshListSpawned() |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var result = SshBridgeClient.ListSpawned(); |
| return $"π Background processes:\n{result}"; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Spawn a background process with a trackable name. Use SshListSpawned to see status and SshKillSpawned to terminate.")] |
| public static string SshSpawn( |
| [Description("Name to identify this background process")] string name, |
| [Description("Command to run in background")] string command) |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var result = SshBridgeClient.Spawn(name, command); |
| return $"π {result}"; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Kill a background process by name (spawned via SshSpawn or matching window title).")] |
| public static string SshKillSpawned( |
| [Description("Name of the background process to kill")] string name) |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var result = SshBridgeClient.KillSpawned(name); |
| return $"π« {result}"; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Write content to a file on the remote system without shell escaping. Perfect for batch files, configs, etc.")] |
| public static string SshWriteFile( |
| [Description("Full path to file (e.g., C:\\scripts\\run.bat)")] string path, |
| [Description("Content to write - characters like & | < > will NOT be escaped")] string content) |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var result = SshBridgeClient.WriteFile(path, content); |
| return $"π {result}"; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
|
|
| [McpServerTool, Description("Append content to a file on the remote system without shell escaping.")] |
| public static string SshAppendFile( |
| [Description("Full path to file")] string path, |
| [Description("Content to append - characters like & | < > will NOT be escaped")] string content) |
| { |
| try |
| { |
| if (!SshBridgeClient.IsBridgeRunning()) |
| { |
| return "β SSH Bridge not running"; |
| } |
|
|
| var result = SshBridgeClient.AppendFile(path, content); |
| return $"π {result}"; |
| } |
| catch |
| { |
| return "β SSH Bridge not running"; |
| } |
| } |
| } |
| } |
|
|