bradlives commited on
Commit
4945f68
·
verified ·
1 Parent(s): 115bf71

Upload 5 files

Browse files
Files changed (5) hide show
  1. README.md +130 -3
  2. ShellMcp.cs +604 -0
  3. ShellMcp.csproj +23 -0
  4. SshBridge.cs +414 -0
  5. SshBridge.csproj +23 -0
README.md CHANGED
@@ -1,3 +1,130 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Shell MCP Server
2
+
3
+ Terminal access for Claude with two security modes, plus SSH bridge for remote servers.
4
+
5
+ ## Features
6
+
7
+ - **Local shell** with safe/dangerous command separation
8
+ - **SSH Bridge** - GUI app for secure remote server access
9
+ - **Full visibility** - see every command Claude runs
10
+ - **Instant disconnect** - revoke access anytime
11
+
12
+ ## Components
13
+
14
+ ### 1. Shell MCP (`shell-mcp.dll`)
15
+ Local Windows terminal access with configurable command allowlists.
16
+
17
+ ### 2. SSH Bridge (`ssh-bridge.exe`)
18
+ WinForms app that:
19
+ - You authenticate with password (never stored on disk)
20
+ - Claude sends commands through it
21
+ - You see all commands and output in real-time
22
+ - Click Disconnect to revoke access instantly
23
+
24
+ ## Installation
25
+
26
+ ### Prerequisites
27
+ - .NET 8.0 SDK
28
+ - Windows 10/11
29
+
30
+ ### Build
31
+
32
+ ```bash
33
+ git clone https://github.com/FreeOnlineUser/shell-mcp.git
34
+ cd shell-mcp
35
+ dotnet restore
36
+ dotnet build ShellMcp.csproj
37
+ dotnet build SshBridge.csproj
38
+ ```
39
+
40
+ ### Configure Claude Desktop
41
+
42
+ Edit `%APPDATA%\Claude\claude_desktop_config.json`:
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "shell_safe": {
48
+ "command": "dotnet",
49
+ "args": ["C:\\path\\to\\shell-mcp.dll"],
50
+ "env": {
51
+ "SHELL_MCP_MODE": "safe",
52
+ "SHELL_MCP_START_DIR": "C:\\your\\workspace"
53
+ }
54
+ },
55
+ "shell_dangerous": {
56
+ "command": "dotnet",
57
+ "args": ["C:\\path\\to\\shell-mcp.dll"],
58
+ "env": {
59
+ "SHELL_MCP_MODE": "dangerous",
60
+ "SHELL_MCP_START_DIR": "C:\\your\\workspace"
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ **Approval settings:**
68
+ - `shell_safe` → "Allow always"
69
+ - `shell_dangerous` → "Allow once" (asks every time)
70
+
71
+ ## Tools
72
+
73
+ ### Local Shell
74
+ | Tool | Description |
75
+ |------|-------------|
76
+ | `Shell` | Execute a local command |
77
+ | `Pwd` | Get current working directory |
78
+ | `ShellInfo` | Show mode and allowed commands |
79
+ | `ShellBatch` | Run multiple commands in sequence |
80
+
81
+ ### SSH (requires SSH Bridge running)
82
+ | Tool | Description |
83
+ |------|-------------|
84
+ | `SshCommand` | Execute command on remote server |
85
+ | `SshStatus` | Check if SSH Bridge is connected |
86
+
87
+ ## SSH Bridge Usage
88
+
89
+ 1. Run `ssh-bridge.exe`
90
+ 2. Enter host, username, and password
91
+ 3. Click **Connect**
92
+ 4. Window shows all commands Claude runs and their output
93
+ 5. Click **Disconnect** anytime to revoke access
94
+
95
+ Password is held in memory only while connected - never written to disk.
96
+
97
+ ## Security Model
98
+
99
+ ### shell_safe (approve once)
100
+ Read-only and build commands:
101
+ - `dir`, `ls`, `type`, `cat`, `pwd`, `cd`
102
+ - `git status`, `git log`, `git diff`, `git branch`
103
+ - `dotnet build`, `dotnet run`, `dotnet test`
104
+ - `npm install`, `npm run`, `npm test`
105
+
106
+ ### shell_dangerous (approve each time)
107
+ Modifying commands:
108
+ - `del`, `rm`, `rmdir`, `move`, `copy`, `mkdir`
109
+ - `git push`, `git commit`, `git reset`
110
+ - `taskkill`
111
+
112
+ ### Always blocked
113
+ - `format`, `diskpart`, `regedit`
114
+ - `net user`, `net localgroup`
115
+ - `rm -rf /`, `del /s /q c:\`
116
+
117
+ ### SSH Bridge
118
+ - You authenticate manually each session
119
+ - You see every command in real-time
120
+ - Disconnect button = instant revoke
121
+ - No password persistence
122
+
123
+ ## Dependencies
124
+
125
+ - [ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol) - MCP SDK for .NET
126
+ - [SSH.NET](https://www.nuget.org/packages/SSH.NET) - SSH client library
127
+
128
+ ## License
129
+
130
+ MIT
ShellMcp.cs ADDED
@@ -0,0 +1,604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.ComponentModel;
4
+ using System.Diagnostics;
5
+ using System.IO;
6
+ using System.IO.Pipes;
7
+ using System.Linq;
8
+ using System.Text;
9
+ using System.Text.Json;
10
+ using System.Threading;
11
+ using System.Threading.Tasks;
12
+ using Microsoft.Extensions.DependencyInjection;
13
+ using Microsoft.Extensions.Hosting;
14
+ using Microsoft.Extensions.Logging;
15
+ using ModelContextProtocol.Server;
16
+
17
+ namespace ShellMcp
18
+ {
19
+ public class Program
20
+ {
21
+ public static async Task Main(string[] args)
22
+ {
23
+ var mode = Environment.GetEnvironmentVariable("SHELL_MCP_MODE") ?? "safe";
24
+ ShellExecutor.Initialize(mode);
25
+
26
+ var builder = Host.CreateApplicationBuilder(args);
27
+
28
+ builder.Logging.ClearProviders();
29
+ builder.Logging.AddConsole(options =>
30
+ {
31
+ options.LogToStandardErrorThreshold = LogLevel.Trace;
32
+ });
33
+
34
+ builder.Services
35
+ .AddMcpServer()
36
+ .WithStdioServerTransport()
37
+ .WithToolsFromAssembly();
38
+
39
+ await builder.Build().RunAsync();
40
+ }
41
+ }
42
+
43
+ // ===============================================
44
+ // SHELL EXECUTOR
45
+ // ===============================================
46
+
47
+ public static class ShellExecutor
48
+ {
49
+ private static string _currentDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
50
+ private static string _mode = "safe";
51
+ private static int _defaultTimeout = 30;
52
+
53
+ private static readonly HashSet<string> SafeCommands = new(StringComparer.OrdinalIgnoreCase)
54
+ {
55
+ "dir", "ls", "pwd", "cd", "tree",
56
+ "type", "cat", "head", "tail", "more", "less", "find", "findstr", "grep", "where", "which",
57
+ "echo", "date", "time", "whoami", "hostname", "ver",
58
+ "git status", "git log", "git diff", "git branch", "git remote", "git fetch", "git show", "git ls-files", "git stash list",
59
+ "dotnet build", "dotnet run", "dotnet test", "dotnet restore", "dotnet clean", "dotnet --version", "dotnet --list-sdks",
60
+ "npm install", "npm run", "npm test", "npm list", "npm --version", "npm ci", "npm audit",
61
+ "node --version", "yarn --version", "yarn install", "yarn build", "yarn test",
62
+ "cls", "clear", "help", "man",
63
+ };
64
+
65
+ private static readonly HashSet<string> DangerousCommands = new(StringComparer.OrdinalIgnoreCase)
66
+ {
67
+ "del", "rm", "rmdir", "rd", "erase",
68
+ "move", "mv", "rename", "ren",
69
+ "copy", "cp", "xcopy", "robocopy",
70
+ "mkdir", "md",
71
+ "git push", "git pull", "git merge", "git rebase", "git reset", "git clean", "git checkout", "git commit", "git add", "git rm", "git stash",
72
+ "taskkill", "kill", "shutdown", "restart",
73
+ "npm install -g", "npm uninstall", "dotnet tool install",
74
+ };
75
+
76
+ private static readonly HashSet<string> BlockedCommands = new(StringComparer.OrdinalIgnoreCase)
77
+ {
78
+ "format", "diskpart", "reg", "regedit", "net user", "net localgroup",
79
+ "powershell -enc", "cmd /c", "rm -rf /", "del /s /q c:\\",
80
+ };
81
+
82
+ public static void Initialize(string mode)
83
+ {
84
+ _mode = mode.ToLowerInvariant();
85
+ var startDir = Environment.GetEnvironmentVariable("SHELL_MCP_START_DIR");
86
+ if (!string.IsNullOrEmpty(startDir) && Directory.Exists(startDir))
87
+ _currentDirectory = startDir;
88
+
89
+ // Set SSH Bridge path from environment
90
+ var bridgePath = Environment.GetEnvironmentVariable("SSH_BRIDGE_PATH");
91
+ if (!string.IsNullOrEmpty(bridgePath))
92
+ SshBridgeClient.SetBridgePath(bridgePath);
93
+ }
94
+
95
+ public static string Mode => _mode;
96
+ public static string CurrentDirectory => _currentDirectory;
97
+
98
+ public static bool IsCommandAllowed(string command, out string reason)
99
+ {
100
+ reason = "";
101
+ string cmdLower = command.ToLowerInvariant().Trim();
102
+ string firstWord = cmdLower.Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? "";
103
+
104
+ foreach (var blocked in BlockedCommands)
105
+ {
106
+ if (cmdLower.Contains(blocked.ToLowerInvariant()))
107
+ {
108
+ reason = $"Command '{blocked}' is blocked for safety";
109
+ return false;
110
+ }
111
+ }
112
+
113
+ if (_mode == "safe")
114
+ {
115
+ bool isSafe = SafeCommands.Any(safe =>
116
+ cmdLower.StartsWith(safe.ToLowerInvariant()) ||
117
+ firstWord == safe.ToLowerInvariant().Split(' ')[0]);
118
+
119
+ if (!isSafe)
120
+ {
121
+ reason = $"Command '{firstWord}' is not in the safe list. Use shell_dangerous for this operation.";
122
+ return false;
123
+ }
124
+ }
125
+ else if (_mode == "dangerous")
126
+ {
127
+ bool isAllowed = SafeCommands.Any(safe =>
128
+ cmdLower.StartsWith(safe.ToLowerInvariant()) ||
129
+ firstWord == safe.ToLowerInvariant().Split(' ')[0]) ||
130
+ DangerousCommands.Any(dangerous =>
131
+ cmdLower.StartsWith(dangerous.ToLowerInvariant()) ||
132
+ firstWord == dangerous.ToLowerInvariant().Split(' ')[0]);
133
+
134
+ if (!isAllowed)
135
+ {
136
+ reason = $"Command '{firstWord}' is not recognized.";
137
+ return false;
138
+ }
139
+ }
140
+
141
+ return true;
142
+ }
143
+
144
+ public static CommandResult Execute(string command, int? timeoutSeconds = null)
145
+ {
146
+ var result = new CommandResult { Command = command };
147
+ int timeout = timeoutSeconds ?? _defaultTimeout;
148
+
149
+ try
150
+ {
151
+ if (command.Trim().StartsWith("cd ", StringComparison.OrdinalIgnoreCase) ||
152
+ command.Trim().Equals("cd", StringComparison.OrdinalIgnoreCase))
153
+ {
154
+ return HandleCd(command);
155
+ }
156
+
157
+ if (!IsCommandAllowed(command, out string reason))
158
+ {
159
+ result.Success = false;
160
+ result.Stderr = reason;
161
+ result.ExitCode = -1;
162
+ return result;
163
+ }
164
+
165
+ var psi = new ProcessStartInfo
166
+ {
167
+ FileName = "cmd.exe",
168
+ Arguments = $"/c {command}",
169
+ WorkingDirectory = _currentDirectory,
170
+ UseShellExecute = false,
171
+ RedirectStandardOutput = true,
172
+ RedirectStandardError = true,
173
+ CreateNoWindow = true,
174
+ StandardOutputEncoding = Encoding.UTF8,
175
+ StandardErrorEncoding = Encoding.UTF8,
176
+ };
177
+
178
+ using var process = new Process { StartInfo = psi };
179
+ var stdout = new StringBuilder();
180
+ var stderr = new StringBuilder();
181
+
182
+ process.OutputDataReceived += (s, e) => { if (e.Data != null) stdout.AppendLine(e.Data); };
183
+ process.ErrorDataReceived += (s, e) => { if (e.Data != null) stderr.AppendLine(e.Data); };
184
+
185
+ var sw = Stopwatch.StartNew();
186
+ process.Start();
187
+ process.BeginOutputReadLine();
188
+ process.BeginErrorReadLine();
189
+
190
+ bool completed = process.WaitForExit(timeout * 1000);
191
+ sw.Stop();
192
+
193
+ if (!completed)
194
+ {
195
+ try { process.Kill(entireProcessTree: true); } catch { }
196
+ result.Success = false;
197
+ result.Stderr = $"Command timed out after {timeout} seconds";
198
+ result.ExitCode = -1;
199
+ result.TimedOut = true;
200
+ }
201
+ else
202
+ {
203
+ result.Success = process.ExitCode == 0;
204
+ result.ExitCode = process.ExitCode;
205
+ result.Stdout = stdout.ToString().TrimEnd();
206
+ result.Stderr = stderr.ToString().TrimEnd();
207
+ }
208
+
209
+ result.ExecutionTimeMs = sw.ElapsedMilliseconds;
210
+ result.WorkingDirectory = _currentDirectory;
211
+ }
212
+ catch (Exception ex)
213
+ {
214
+ result.Success = false;
215
+ result.Stderr = $"Execution error: {ex.Message}";
216
+ result.ExitCode = -1;
217
+ }
218
+
219
+ return result;
220
+ }
221
+
222
+ private static CommandResult HandleCd(string command)
223
+ {
224
+ var result = new CommandResult { Command = command };
225
+ var parts = command.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
226
+
227
+ if (parts.Length == 1)
228
+ {
229
+ result.Success = true;
230
+ result.Stdout = _currentDirectory;
231
+ result.WorkingDirectory = _currentDirectory;
232
+ return result;
233
+ }
234
+
235
+ string targetPath = parts[1].Trim().Trim('"');
236
+ string newPath;
237
+
238
+ if (Path.IsPathRooted(targetPath))
239
+ newPath = targetPath;
240
+ else if (targetPath == "..")
241
+ newPath = Path.GetDirectoryName(_currentDirectory) ?? _currentDirectory;
242
+ else if (targetPath == "~")
243
+ newPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
244
+ else
245
+ newPath = Path.Combine(_currentDirectory, targetPath);
246
+
247
+ newPath = Path.GetFullPath(newPath);
248
+
249
+ if (Directory.Exists(newPath))
250
+ {
251
+ _currentDirectory = newPath;
252
+ result.Success = true;
253
+ result.Stdout = _currentDirectory;
254
+ result.WorkingDirectory = _currentDirectory;
255
+ }
256
+ else
257
+ {
258
+ result.Success = false;
259
+ result.Stderr = $"Directory not found: {newPath}";
260
+ result.ExitCode = 1;
261
+ result.WorkingDirectory = _currentDirectory;
262
+ }
263
+
264
+ return result;
265
+ }
266
+
267
+ public static string GetAllowedCommands()
268
+ {
269
+ var sb = new StringBuilder();
270
+ sb.AppendLine($"Mode: {_mode}");
271
+ sb.AppendLine();
272
+ sb.AppendLine("Safe commands (always allowed):");
273
+ foreach (var cmd in SafeCommands.OrderBy(c => c))
274
+ sb.AppendLine($" - {cmd}");
275
+
276
+ if (_mode == "dangerous")
277
+ {
278
+ sb.AppendLine();
279
+ sb.AppendLine("Dangerous commands (available in this mode):");
280
+ foreach (var cmd in DangerousCommands.OrderBy(c => c))
281
+ sb.AppendLine($" - {cmd}");
282
+ }
283
+
284
+ sb.AppendLine();
285
+ sb.AppendLine("Blocked commands (never allowed):");
286
+ foreach (var cmd in BlockedCommands.OrderBy(c => c))
287
+ sb.AppendLine($" - {cmd}");
288
+
289
+ return sb.ToString();
290
+ }
291
+ }
292
+
293
+ public class CommandResult
294
+ {
295
+ public string Command { get; set; } = "";
296
+ public bool Success { get; set; }
297
+ public string Stdout { get; set; } = "";
298
+ public string Stderr { get; set; } = "";
299
+ public int ExitCode { get; set; }
300
+ public long ExecutionTimeMs { get; set; }
301
+ public string WorkingDirectory { get; set; } = "";
302
+ public bool TimedOut { get; set; }
303
+
304
+ public override string ToString()
305
+ {
306
+ var sb = new StringBuilder();
307
+ if (!string.IsNullOrEmpty(Stdout))
308
+ sb.AppendLine(Stdout);
309
+ if (!string.IsNullOrEmpty(Stderr))
310
+ {
311
+ if (sb.Length > 0) sb.AppendLine();
312
+ sb.AppendLine($"[stderr] {Stderr}");
313
+ }
314
+ sb.AppendLine();
315
+ sb.AppendLine($"[exit: {ExitCode}] [time: {ExecutionTimeMs}ms] [cwd: {WorkingDirectory}]");
316
+ if (TimedOut)
317
+ sb.AppendLine("[TIMED OUT]");
318
+ return sb.ToString();
319
+ }
320
+ }
321
+
322
+ // ===============================================
323
+ // SSH BRIDGE CLIENT
324
+ // ===============================================
325
+
326
+ public static class SshBridgeClient
327
+ {
328
+ private const int PORT = 52718;
329
+
330
+ private static string? _bridgePath;
331
+
332
+ public static void SetBridgePath(string path)
333
+ {
334
+ _bridgePath = path;
335
+ }
336
+
337
+ public static string GetStatus()
338
+ {
339
+ try
340
+ {
341
+ return SendCommand("__STATUS__");
342
+ }
343
+ catch
344
+ {
345
+ return "DISCONNECTED";
346
+ }
347
+ }
348
+
349
+ public static bool LaunchBridge()
350
+ {
351
+ if (string.IsNullOrEmpty(_bridgePath) || !System.IO.File.Exists(_bridgePath))
352
+ return false;
353
+
354
+ try
355
+ {
356
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
357
+ {
358
+ FileName = _bridgePath,
359
+ UseShellExecute = true
360
+ });
361
+ return true;
362
+ }
363
+ catch
364
+ {
365
+ return false;
366
+ }
367
+ }
368
+
369
+ public static bool IsBridgeRunning()
370
+ {
371
+ try
372
+ {
373
+ using var client = new System.Net.Sockets.TcpClient();
374
+ client.Connect("127.0.0.1", PORT);
375
+ return true;
376
+ }
377
+ catch
378
+ {
379
+ return false;
380
+ }
381
+ }
382
+
383
+ public static string SendCommand(string command)
384
+ {
385
+ using var client = new System.Net.Sockets.TcpClient();
386
+ client.Connect("127.0.0.1", PORT);
387
+
388
+ using var stream = client.GetStream();
389
+ using var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
390
+ using var reader = new StreamReader(stream, Encoding.UTF8);
391
+
392
+ writer.WriteLine(command);
393
+ var response = reader.ReadLine() ?? "No response";
394
+ // Decode newlines
395
+ return response.Replace("<<CRLF>>", "\r\n").Replace("<<LF>>", "\n");
396
+ }
397
+
398
+ public static string Prefill(string host, int port, string user, string? password = null)
399
+ {
400
+ var cmd = $"__PREFILL__:{host}:{port}:{user}";
401
+ if (!string.IsNullOrEmpty(password))
402
+ cmd += $":{password}";
403
+ return SendCommand(cmd);
404
+ }
405
+
406
+ public static string TriggerConnect()
407
+ {
408
+ return SendCommand("__CONNECT__");
409
+ }
410
+ }
411
+
412
+ // ===============================================
413
+ // MCP TOOLS
414
+ // ===============================================
415
+
416
+ [McpServerToolType]
417
+ public static class ShellTools
418
+ {
419
+ [McpServerTool, Description("Execute a shell command. Working directory persists across calls. Use 'cd' to navigate. Check 'shell_info' for allowed commands.")]
420
+ public static string Shell(
421
+ [Description("Command to execute")] string command,
422
+ [Description("Timeout in seconds (default: 30)")] int? timeout = null)
423
+ {
424
+ if (string.IsNullOrWhiteSpace(command))
425
+ return "Error: No command provided";
426
+
427
+ var result = ShellExecutor.Execute(command, timeout);
428
+ return result.Success ? result.ToString() : $"❌ Command failed:\n{result}";
429
+ }
430
+
431
+ [McpServerTool, Description("Get current working directory")]
432
+ public static string Pwd()
433
+ {
434
+ return ShellExecutor.CurrentDirectory;
435
+ }
436
+
437
+ [McpServerTool, Description("Show shell mode and list of allowed commands")]
438
+ public static string ShellInfo()
439
+ {
440
+ var sb = new StringBuilder();
441
+ sb.AppendLine($"Shell MCP - {ShellExecutor.Mode.ToUpperInvariant()} mode");
442
+ sb.AppendLine($"Current directory: {ShellExecutor.CurrentDirectory}");
443
+ sb.AppendLine();
444
+ sb.AppendLine(ShellExecutor.GetAllowedCommands());
445
+ return sb.ToString();
446
+ }
447
+
448
+ [McpServerTool, Description("Execute multiple commands in sequence. Stops on first failure unless continue_on_error is true.")]
449
+ public static string ShellBatch(
450
+ [Description("JSON array of commands: [\"cmd1\", \"cmd2\", ...]")] string commands_json,
451
+ [Description("Continue executing even if a command fails")] bool continue_on_error = false,
452
+ [Description("Timeout per command in seconds")] int? timeout = null)
453
+ {
454
+ try
455
+ {
456
+ var commands = JsonSerializer.Deserialize<List<string>>(commands_json);
457
+ if (commands == null || commands.Count == 0)
458
+ return "Error: No commands provided";
459
+
460
+ var sb = new StringBuilder();
461
+ int succeeded = 0, failed = 0;
462
+
463
+ foreach (var cmd in commands)
464
+ {
465
+ sb.AppendLine($"$ {cmd}");
466
+ var result = ShellExecutor.Execute(cmd, timeout);
467
+ sb.AppendLine(result.ToString());
468
+
469
+ if (result.Success)
470
+ succeeded++;
471
+ else
472
+ {
473
+ failed++;
474
+ if (!continue_on_error)
475
+ {
476
+ sb.AppendLine($"❌ Batch stopped. {succeeded} succeeded, {failed} failed, {commands.Count - succeeded - failed} skipped.");
477
+ return sb.ToString();
478
+ }
479
+ }
480
+ }
481
+
482
+ sb.AppendLine($"✅ Batch complete: {succeeded} succeeded, {failed} failed");
483
+ return sb.ToString();
484
+ }
485
+ catch (Exception ex)
486
+ {
487
+ return $"Error parsing commands: {ex.Message}";
488
+ }
489
+ }
490
+
491
+ [McpServerTool, Description("Execute a command on a remote server via SSH Bridge. Requires SSH Bridge app to be running and connected.")]
492
+ public static string SshCommand(
493
+ [Description("Command to execute on the remote server")] string command)
494
+ {
495
+ if (string.IsNullOrWhiteSpace(command))
496
+ return "Error: No command provided";
497
+
498
+ try
499
+ {
500
+ // Check if bridge is running, launch if not
501
+ if (!SshBridgeClient.IsBridgeRunning())
502
+ {
503
+ if (SshBridgeClient.LaunchBridge())
504
+ {
505
+ 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";
506
+ }
507
+ else
508
+ {
509
+ 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.";
510
+ }
511
+ }
512
+
513
+ var status = SshBridgeClient.GetStatus();
514
+ if (status == "DISCONNECTED")
515
+ {
516
+ return "⚠️ SSH Bridge is open but not connected.\n\nPlease:\n1. Enter host, user, and password\n2. Click Connect\n3. Try this command again";
517
+ }
518
+
519
+ var result = SshBridgeClient.SendCommand(command);
520
+ return $"📡 {status}\n\n{result}";
521
+ }
522
+ catch (Exception ex)
523
+ {
524
+ return $"❌ SSH error: {ex.Message}";
525
+ }
526
+ }
527
+
528
+ [McpServerTool, Description("Check if SSH Bridge is connected")]
529
+ public static string SshStatus()
530
+ {
531
+ try
532
+ {
533
+ if (!SshBridgeClient.IsBridgeRunning())
534
+ {
535
+ return "❌ SSH Bridge not running";
536
+ }
537
+
538
+ var status = SshBridgeClient.GetStatus();
539
+ if (status.StartsWith("CONNECTED:"))
540
+ {
541
+ return $"✅ {status.Replace("CONNECTED:", "Connected to ")}";
542
+ }
543
+ return "⚠️ SSH Bridge open but not connected";
544
+ }
545
+ catch
546
+ {
547
+ return "❌ SSH Bridge not running";
548
+ }
549
+ }
550
+
551
+ [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.")]
552
+ public static string SshPrefill(
553
+ [Description("SSH host/IP address")] string host,
554
+ [Description("SSH username")] string user,
555
+ [Description("SSH port (default: 22)")] int port = 22,
556
+ [Description("SSH password (optional - user can enter manually for security)")] string? password = null,
557
+ [Description("Automatically click Connect after prefilling")] bool auto_connect = false)
558
+ {
559
+ try
560
+ {
561
+ // Launch bridge if not running
562
+ if (!SshBridgeClient.IsBridgeRunning())
563
+ {
564
+ if (!SshBridgeClient.LaunchBridge())
565
+ {
566
+ return "❌ Could not launch SSH Bridge. Set SSH_BRIDGE_PATH in your Claude config.";
567
+ }
568
+ // Wait for it to start
569
+ Thread.Sleep(1000);
570
+ }
571
+
572
+ // Prefill the fields
573
+ var result = SshBridgeClient.Prefill(host, port, user, password);
574
+ if (result != "PREFILLED")
575
+ {
576
+ return $"❌ Prefill failed: {result}";
577
+ }
578
+
579
+ if (auto_connect && !string.IsNullOrEmpty(password))
580
+ {
581
+ Thread.Sleep(200);
582
+ SshBridgeClient.TriggerConnect();
583
+ Thread.Sleep(2000); // Wait for connection
584
+ var status = SshBridgeClient.GetStatus();
585
+ if (status.StartsWith("CONNECTED:"))
586
+ {
587
+ return $"✅ Connected to {user}@{host}:{port}";
588
+ }
589
+ return $"⚠️ Connection initiated. Status: {status}";
590
+ }
591
+
592
+ if (string.IsNullOrEmpty(password))
593
+ {
594
+ return $"📝 SSH Bridge prefilled with {user}@{host}:{port}\n\nPlease enter password and click Connect.";
595
+ }
596
+ return $"📝 SSH Bridge prefilled with {user}@{host}:{port}\n\nClick Connect when ready.";
597
+ }
598
+ catch (Exception ex)
599
+ {
600
+ return $"❌ Error: {ex.Message}";
601
+ }
602
+ }
603
+ }
604
+ }
ShellMcp.csproj ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+
3
+ <PropertyGroup>
4
+ <OutputType>Exe</OutputType>
5
+ <TargetFramework>net8.0</TargetFramework>
6
+ <ImplicitUsings>disable</ImplicitUsings>
7
+ <Nullable>enable</Nullable>
8
+ <AssemblyName>shell-mcp</AssemblyName>
9
+ <RootNamespace>ShellMcp</RootNamespace>
10
+ </PropertyGroup>
11
+
12
+ <!-- Only compile ShellMcp.cs -->
13
+ <ItemGroup>
14
+ <Compile Remove="*.cs" />
15
+ <Compile Include="ShellMcp.cs" />
16
+ </ItemGroup>
17
+
18
+ <ItemGroup>
19
+ <PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.10" />
20
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
21
+ </ItemGroup>
22
+
23
+ </Project>
SshBridge.cs ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System;
2
+ using System.Drawing;
3
+ using System.IO;
4
+ using System.Net;
5
+ using System.Net.Sockets;
6
+ using System.Text;
7
+ using System.Threading;
8
+ using System.Threading.Tasks;
9
+ using System.Windows.Forms;
10
+ using Renci.SshNet;
11
+
12
+ namespace SshBridge
13
+ {
14
+ public class Program
15
+ {
16
+ [STAThread]
17
+ public static void Main(string[] args)
18
+ {
19
+ Application.EnableVisualStyles();
20
+ Application.SetCompatibleTextRenderingDefault(false);
21
+ Application.Run(new SshBridgeForm());
22
+ }
23
+ }
24
+
25
+ public class SshBridgeForm : Form
26
+ {
27
+ private RichTextBox _outputBox = null!;
28
+ private TextBox _hostBox = null!;
29
+ private TextBox _portBox = null!;
30
+ private TextBox _userBox = null!;
31
+ private TextBox _passwordBox = null!;
32
+ private Button _connectButton = null!;
33
+ private Button _disconnectButton = null!;
34
+ private Label _statusLabel = null!;
35
+ private Panel _loginPanel = null!;
36
+ private Panel _sessionPanel = null!;
37
+
38
+ private Thread? _serverThread;
39
+ private CancellationTokenSource? _cts;
40
+ private SshClient? _sshClient;
41
+ private bool _isConnected;
42
+ private int _commandCount;
43
+ private string _currentHost = "";
44
+ private string _currentUser = "";
45
+
46
+ private const int PORT = 52718;
47
+
48
+ public SshBridgeForm()
49
+ {
50
+ InitializeComponents();
51
+ StartTcpServer();
52
+ }
53
+
54
+ private void InitializeComponents()
55
+ {
56
+ this.Text = "SSH Bridge for Claude";
57
+ this.Size = new Size(700, 500);
58
+ this.MinimumSize = new Size(500, 400);
59
+ this.StartPosition = FormStartPosition.CenterScreen;
60
+ this.FormBorderStyle = FormBorderStyle.Sizable;
61
+
62
+ // Login Panel
63
+ _loginPanel = new Panel
64
+ {
65
+ Dock = DockStyle.Fill,
66
+ Padding = new Padding(20),
67
+ };
68
+
69
+ var loginLayout = new TableLayoutPanel
70
+ {
71
+ Dock = DockStyle.Fill,
72
+ ColumnCount = 2,
73
+ RowCount = 6,
74
+ Padding = new Padding(50, 30, 50, 30),
75
+ };
76
+ loginLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 100));
77
+ loginLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
78
+ loginLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
79
+ loginLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
80
+ loginLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
81
+ loginLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
82
+ loginLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 50));
83
+ loginLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
84
+
85
+ loginLayout.Controls.Add(new Label { Text = "Host:", Anchor = AnchorStyles.Left, AutoSize = true }, 0, 0);
86
+ _hostBox = new TextBox { Dock = DockStyle.Fill, Text = "" };
87
+ loginLayout.Controls.Add(_hostBox, 1, 0);
88
+
89
+ loginLayout.Controls.Add(new Label { Text = "Port:", Anchor = AnchorStyles.Left, AutoSize = true }, 0, 1);
90
+ _portBox = new TextBox { Dock = DockStyle.Fill, Text = "22", Width = 80 };
91
+ loginLayout.Controls.Add(_portBox, 1, 1);
92
+
93
+ loginLayout.Controls.Add(new Label { Text = "User:", Anchor = AnchorStyles.Left, AutoSize = true }, 0, 2);
94
+ _userBox = new TextBox { Dock = DockStyle.Fill, Text = "" };
95
+ loginLayout.Controls.Add(_userBox, 1, 2);
96
+
97
+ loginLayout.Controls.Add(new Label { Text = "Password:", Anchor = AnchorStyles.Left, AutoSize = true }, 0, 3);
98
+ _passwordBox = new TextBox { Dock = DockStyle.Fill, UseSystemPasswordChar = true };
99
+ _passwordBox.KeyPress += (s, e) => { if (e.KeyChar == (char)Keys.Enter) Connect(); };
100
+ loginLayout.Controls.Add(_passwordBox, 1, 3);
101
+
102
+ _connectButton = new Button { Text = "Connect", Width = 100, Height = 35 };
103
+ _connectButton.Click += (s, e) => Connect();
104
+ var buttonPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight };
105
+ buttonPanel.Controls.Add(_connectButton);
106
+ loginLayout.Controls.Add(buttonPanel, 1, 4);
107
+
108
+ _loginPanel.Controls.Add(loginLayout);
109
+
110
+ // Session Panel
111
+ _sessionPanel = new Panel
112
+ {
113
+ Dock = DockStyle.Fill,
114
+ Visible = false,
115
+ };
116
+
117
+ var topBar = new Panel
118
+ {
119
+ Dock = DockStyle.Top,
120
+ Height = 40,
121
+ BackColor = Color.FromArgb(45, 45, 48),
122
+ Padding = new Padding(10, 5, 10, 5),
123
+ };
124
+
125
+ _statusLabel = new Label
126
+ {
127
+ Text = "Disconnected",
128
+ ForeColor = Color.White,
129
+ AutoSize = true,
130
+ Location = new Point(10, 10),
131
+ Font = new Font("Segoe UI", 10),
132
+ };
133
+ topBar.Controls.Add(_statusLabel);
134
+
135
+ _disconnectButton = new Button
136
+ {
137
+ Text = "Disconnect",
138
+ ForeColor = Color.White,
139
+ BackColor = Color.FromArgb(180, 50, 50),
140
+ FlatStyle = FlatStyle.Flat,
141
+ Width = 90,
142
+ Height = 28,
143
+ Anchor = AnchorStyles.Right,
144
+ };
145
+ _disconnectButton.Location = new Point(topBar.Width - _disconnectButton.Width - 10, 6);
146
+ _disconnectButton.Click += (s, e) => Disconnect();
147
+ topBar.Controls.Add(_disconnectButton);
148
+ topBar.Resize += (s, e) => _disconnectButton.Location = new Point(topBar.Width - _disconnectButton.Width - 10, 6);
149
+
150
+ _outputBox = new RichTextBox
151
+ {
152
+ Dock = DockStyle.Fill,
153
+ ReadOnly = true,
154
+ BackColor = Color.FromArgb(30, 30, 30),
155
+ ForeColor = Color.FromArgb(220, 220, 220),
156
+ Font = new Font("Consolas", 10),
157
+ WordWrap = false,
158
+ };
159
+
160
+ _sessionPanel.Controls.Add(_outputBox);
161
+ _sessionPanel.Controls.Add(topBar);
162
+
163
+ this.Controls.Add(_loginPanel);
164
+ this.Controls.Add(_sessionPanel);
165
+ }
166
+
167
+ private void Connect()
168
+ {
169
+ string host = _hostBox.Text.Trim();
170
+ string user = _userBox.Text.Trim();
171
+ string password = _passwordBox.Text;
172
+ int port = 22;
173
+ int.TryParse(_portBox.Text.Trim(), out port);
174
+ if (port <= 0 || port > 65535) port = 22;
175
+
176
+ if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(user) || string.IsNullOrEmpty(password))
177
+ {
178
+ MessageBox.Show("Please fill in all fields.", "Connection Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
179
+ return;
180
+ }
181
+
182
+ _connectButton.Enabled = false;
183
+ _connectButton.Text = "Connecting...";
184
+
185
+ Task.Run(() =>
186
+ {
187
+ try
188
+ {
189
+ _sshClient = new SshClient(host, port, user, password);
190
+ _sshClient.ConnectionInfo.Timeout = TimeSpan.FromSeconds(10);
191
+ _sshClient.Connect();
192
+
193
+ if (_sshClient.IsConnected)
194
+ {
195
+ _currentHost = host;
196
+ _currentUser = user;
197
+ _commandCount = 0;
198
+
199
+ this.Invoke(() =>
200
+ {
201
+ _passwordBox.Clear();
202
+ OnConnected();
203
+ });
204
+ }
205
+ else
206
+ {
207
+ throw new Exception("Connection failed");
208
+ }
209
+ }
210
+ catch (Exception ex)
211
+ {
212
+ this.Invoke(() =>
213
+ {
214
+ MessageBox.Show($"Connection failed:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
215
+ _connectButton.Enabled = true;
216
+ _connectButton.Text = "Connect";
217
+ });
218
+ }
219
+ });
220
+ }
221
+
222
+ private void OnConnected()
223
+ {
224
+ _isConnected = true;
225
+ _loginPanel.Visible = false;
226
+ _sessionPanel.Visible = true;
227
+ _statusLabel.Text = $"Connected to {_currentUser}@{_currentHost}";
228
+ _outputBox.Clear();
229
+ AppendOutput($"=== Connected to {_currentUser}@{_currentHost} ===", Color.LimeGreen);
230
+ }
231
+
232
+ private void Disconnect()
233
+ {
234
+ _isConnected = false;
235
+
236
+ try
237
+ {
238
+ _sshClient?.Disconnect();
239
+ _sshClient?.Dispose();
240
+ }
241
+ catch { }
242
+
243
+ _sshClient = null;
244
+ _currentHost = "";
245
+ _currentUser = "";
246
+
247
+ _loginPanel.Visible = true;
248
+ _sessionPanel.Visible = false;
249
+ _connectButton.Enabled = true;
250
+ _connectButton.Text = "Connect";
251
+ _statusLabel.Text = "Disconnected";
252
+ }
253
+
254
+ private void AppendOutput(string? text, Color? color = null)
255
+ {
256
+ if (string.IsNullOrEmpty(text)) return;
257
+
258
+ if (_outputBox.InvokeRequired)
259
+ {
260
+ _outputBox.Invoke(() => AppendOutput(text, color));
261
+ return;
262
+ }
263
+
264
+ // Normalize line endings to Windows style
265
+ var normalized = text.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n");
266
+
267
+ _outputBox.SelectionStart = _outputBox.TextLength;
268
+ _outputBox.SelectionLength = 0;
269
+ _outputBox.SelectionColor = color ?? Color.FromArgb(220, 220, 220);
270
+ _outputBox.AppendText(normalized + "\r\n");
271
+ _outputBox.SelectionColor = _outputBox.ForeColor;
272
+ _outputBox.ScrollToCaret();
273
+ }
274
+
275
+ public string ExecuteCommand(string command)
276
+ {
277
+ if (!_isConnected || _sshClient == null || !_sshClient.IsConnected)
278
+ {
279
+ return "ERROR: Not connected. Open SSH Bridge and connect first.";
280
+ }
281
+
282
+ try
283
+ {
284
+ _commandCount++;
285
+ this.Invoke(() => _statusLabel.Text = $"Connected to {_currentUser}@{_currentHost} ({_commandCount} commands)");
286
+
287
+ AppendOutput($"> {command}", Color.Cyan);
288
+
289
+ using var cmd = _sshClient.CreateCommand(command);
290
+ cmd.CommandTimeout = TimeSpan.FromSeconds(30);
291
+ var result = cmd.Execute();
292
+ var error = cmd.Error;
293
+
294
+ var output = result.Trim();
295
+ if (!string.IsNullOrEmpty(error))
296
+ {
297
+ output += "\n[stderr] " + error.Trim();
298
+ }
299
+
300
+ if (string.IsNullOrEmpty(output))
301
+ {
302
+ output = "(no output)";
303
+ }
304
+
305
+ AppendOutput(output);
306
+
307
+ return output;
308
+ }
309
+ catch (Exception ex)
310
+ {
311
+ var error = $"ERROR: {ex.Message}";
312
+ AppendOutput(error, Color.Red);
313
+ return error;
314
+ }
315
+ }
316
+
317
+ private void StartTcpServer()
318
+ {
319
+ _cts = new CancellationTokenSource();
320
+ _serverThread = new Thread(() => TcpServerLoop(_cts.Token))
321
+ {
322
+ IsBackground = true,
323
+ Name = "SshBridge TCP Server"
324
+ };
325
+ _serverThread.Start();
326
+ }
327
+
328
+ private void TcpServerLoop(CancellationToken ct)
329
+ {
330
+ var listener = new TcpListener(IPAddress.Loopback, PORT);
331
+ listener.Start();
332
+
333
+ while (!ct.IsCancellationRequested)
334
+ {
335
+ try
336
+ {
337
+ if (!listener.Pending())
338
+ {
339
+ Thread.Sleep(50);
340
+ continue;
341
+ }
342
+
343
+ using var client = listener.AcceptTcpClient();
344
+ using var stream = client.GetStream();
345
+ using var reader = new StreamReader(stream, Encoding.UTF8);
346
+ using var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
347
+
348
+ string? command = reader.ReadLine();
349
+ if (!string.IsNullOrEmpty(command))
350
+ {
351
+ string response;
352
+ if (command == "__STATUS__")
353
+ {
354
+ response = _isConnected ? $"CONNECTED:{_currentUser}@{_currentHost}" : "DISCONNECTED";
355
+ }
356
+ else if (command.StartsWith("__PREFILL__:"))
357
+ {
358
+ // Format: __PREFILL__:host:port:user:password
359
+ // Password is optional - if not provided, user must enter it
360
+ var parts = command.Substring(12).Split(':', 4);
361
+ if (parts.Length >= 3)
362
+ {
363
+ this.Invoke(() =>
364
+ {
365
+ _hostBox.Text = parts[0];
366
+ _portBox.Text = parts[1];
367
+ _userBox.Text = parts[2];
368
+ if (parts.Length >= 4 && !string.IsNullOrEmpty(parts[3]))
369
+ {
370
+ _passwordBox.Text = parts[3];
371
+ }
372
+ _passwordBox.Focus();
373
+ this.Activate();
374
+ this.BringToFront();
375
+ });
376
+ response = "PREFILLED";
377
+ }
378
+ else
379
+ {
380
+ response = "ERROR: Invalid prefill format. Use __PREFILL__:host:port:user[:password]";
381
+ }
382
+ }
383
+ else if (command == "__CONNECT__")
384
+ {
385
+ // Trigger connect if fields are filled
386
+ this.Invoke(() => Connect());
387
+ response = "CONNECTING";
388
+ }
389
+ else
390
+ {
391
+ response = ExecuteCommand(command);
392
+ }
393
+ // Encode newlines so they survive the single-line protocol
394
+ response = response.Replace("\r\n", "<<CRLF>>").Replace("\n", "<<LF>>");
395
+ writer.WriteLine(response);
396
+ }
397
+ }
398
+ catch (Exception)
399
+ {
400
+ // Ignore errors
401
+ }
402
+ }
403
+
404
+ listener.Stop();
405
+ }
406
+
407
+ protected override void OnFormClosing(FormClosingEventArgs e)
408
+ {
409
+ _cts?.Cancel();
410
+ Disconnect();
411
+ base.OnFormClosing(e);
412
+ }
413
+ }
414
+ }
SshBridge.csproj ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+
3
+ <PropertyGroup>
4
+ <OutputType>WinExe</OutputType>
5
+ <TargetFramework>net8.0-windows</TargetFramework>
6
+ <UseWindowsForms>true</UseWindowsForms>
7
+ <ImplicitUsings>disable</ImplicitUsings>
8
+ <Nullable>enable</Nullable>
9
+ <AssemblyName>ssh-bridge</AssemblyName>
10
+ <RootNamespace>SshBridge</RootNamespace>
11
+ </PropertyGroup>
12
+
13
+ <!-- Only compile SshBridge.cs -->
14
+ <ItemGroup>
15
+ <Compile Remove="*.cs" />
16
+ <Compile Include="SshBridge.cs" />
17
+ </ItemGroup>
18
+
19
+ <ItemGroup>
20
+ <PackageReference Include="SSH.NET" Version="2024.2.0" />
21
+ </ItemGroup>
22
+
23
+ </Project>