diff --git a/Alchegos.MCP.csproj b/Alchegos.MCP.csproj
index 058d402..f1d6f49 100644
--- a/Alchegos.MCP.csproj
+++ b/Alchegos.MCP.csproj
@@ -7,13 +7,17 @@
enable
Linux
-
+
+
+
+
+
diff --git a/Dockerfile b/Dockerfile
index 988a721..f3b76dd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,6 +5,7 @@ WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
+COPY NuGet.Config ./
COPY ["*.sln", "."]
COPY ["Alchegos.MCP.csproj", "./"]
RUN dotnet restore "Alchegos.MCP.csproj"
@@ -19,4 +20,14 @@ RUN dotnet publish "./Alchegos.MCP.csproj" -c $BUILD_CONFIGURATION -o /app/publi
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "Alchegos.MCP.dll"]
+USER root
+RUN apt-get update
+RUN apt-get install -y --no-install-recommends openssh-client openssh-server
+RUN rm -rf /var/lib/apt/lists/*
+RUN mkdir -p /var/run/sshd
+RUN sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
+RUN sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
+
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/Middleware/RequestBodyLoggingMiddleware.cs b/Middleware/RequestBodyLoggingMiddleware.cs
new file mode 100644
index 0000000..af6c89e
--- /dev/null
+++ b/Middleware/RequestBodyLoggingMiddleware.cs
@@ -0,0 +1,51 @@
+using System.Text;
+
+namespace Alchegos.MCP.Middleware
+{
+
+ public class RequestBodyLoggingMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+ public RequestBodyLoggingMiddleware(RequestDelegate next, ILogger logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ if (context.Request.Method == HttpMethods.Post && context.Request.ContentLength > 0)
+ {
+ context.Request.EnableBuffering();
+
+ using var reader = new StreamReader(
+ context.Request.Body,
+ encoding: Encoding.UTF8,
+ detectEncodingFromByteOrderMarks: false,
+ bufferSize: 4096,
+ leaveOpen: true);
+
+ var requestBody = await reader.ReadToEndAsync();
+ context.Request.Body.Position = 0;
+
+ _logger.LogTrace("Request Body (POST {RequestPath}):\n{RequestBody}", context.Request.Path, requestBody);
+
+ }
+
+ await _next(context);
+ }
+ }
+}
+
+namespace Microsoft.AspNetCore.Builder
+{
+ public static class RequestBodyLoggingMiddlewareExtensions
+ {
+ public static IApplicationBuilder UseRequestBodyLogging(
+ this IApplicationBuilder builder)
+ {
+ return builder.UseMiddleware();
+ }
+ }
+}
\ No newline at end of file
diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..e9328aa
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
index 858a873..356ef9b 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,29 +1,19 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Server;
-using System.ComponentModel;
-using System.Diagnostics;
-using Alchegos.MCP.Tools;
-
var builder = WebApplication.CreateBuilder(args);
+const int BIND_PORT = 5050;
+
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly();
-builder.WebHost.ConfigureKestrel(options => options.ListenAnyIP(5050));
+builder.WebHost.ConfigureKestrel(options => options.ListenAnyIP(BIND_PORT));
+//builder.Services.AddHttpLogging(o => { });
var app = builder.Build();
-
+// var logger = app.Logger;
+//app.UseHttpLogging();
+app.UseRequestBodyLogging();
app.MapMcp();
app.Run();
-[McpServerToolType]
-public static class EchoTool
-{
- [McpServerTool, Description("Echoes the message back to the client.")]
- public static string Echo(string message) => $"hello {message}";
-}
-
diff --git a/Tools/GiteaTool.cs b/Tools/GiteaTool.cs
index a6528b7..526033d 100644
--- a/Tools/GiteaTool.cs
+++ b/Tools/GiteaTool.cs
@@ -1,8 +1,18 @@
using ModelContextProtocol.Server;
+using Alchegos.Core.Services.Gitea;
+
namespace Alchegos.MCP.Tools;
[McpServerToolType]
public static class GiteaTool
{
-
+ public static string Login(string a)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static string GetGiteaRepoList(string apiKey)
+ {
+ throw new NotImplementedException();
+ }
}
\ No newline at end of file
diff --git a/Tools/TerminalTool.cs b/Tools/TerminalTool.cs
index 62d0f4b..c161fed 100644
--- a/Tools/TerminalTool.cs
+++ b/Tools/TerminalTool.cs
@@ -6,60 +6,52 @@ using System.Text.RegularExpressions;
using ModelContextProtocol.Server;
namespace Alchegos.MCP.Tools;
-
-[McpServerToolType, Description("Manage multiple persistent terminal sessions, enabling creation, closure, command execution, and output retrieval.")]
+[McpServerToolType, Description("""
+ Manages multiple persistent terminal sessions.
+ **Sessions are identified and operated on using their unique `sessionId`. The `label` is for human reference only.**
+ Enables creation, closure, command execution, and output retrieval.
+ """)]
public static class TerminalTool
{
private static readonly ConcurrentDictionary _sessions = new();
-
public record SessionInfo(
- [property: Description("Unique identifier for the session.")] string SessionId,
- [property: Description("Human-readable label for the session.")] string Label
+ [property: Description("Unique identifier (`sessionId`) for the session. **This ID is required for all operations targeting this specific session (e.g., sending input, exiting).**")] string SessionId,
+ [property: Description("Human-readable label for the session. **This is for display and organizational purposes only and CANNOT be used to interact with the session via subsequent tool calls.**")] string Label
);
-
public record ListSessionsResult(
- [property: Description("List of all active sessions with their IDs and labels.")] List Sessions
+ [property: Description("List of all active sessions, including their `sessionId` and `label`. **Use the `sessionId` from this list for interacting with a specific session in other tool calls.**")] List Sessions
);
-
public record CreateSessionResult(
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status,
[property: Description("Details of the created session if successful; otherwise, with empty fields.")] SessionInfo Session
);
-
public record ExitSessionResult(
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
);
-
public record SendSignalResult(
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
);
-
public record SendInputWithExceptResult(
[property: Description("Indicates how the response was triggered: 'except' when a regex matched, or 'timeout' when the timeout was reached.")] string OutputBy,
[property: Description("The regex pattern that matched, or null if none matched before timeout.")] string? MatchedExcept,
[property: Description("Accumulated standard output from the session after sending input.")] string Stdout,
[property: Description("Accumulated standard error from the session after sending input.")] string Stderr
);
-
public record SendInputAndForgetResult(
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
);
-
public record OneOffCommandResult(
- [property: Description("How the command completed: 'finish' when the process exited normally, or 'timeout' when the timeout was reached.")] string ReturnBy,
+ [property: Description("How the command completed: 'finish' when the process exited normally, or 'timeout' when the timeout was reached. 'error' otherwise")] string ReturnBy,
[property: Description("Standard output produced by the command.")] string Stdout,
[property: Description("Standard error produced by the command.")] string Stderr
);
-
public record GetCachedResultResult(
[property: Description("Cached standard output of the session.")] string Stdout,
[property: Description("Cached standard error of the session.")] string Stderr
);
-
public record ClearResult(
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
);
-
private class TerminalSession : IDisposable
{
public string Id { get; }
@@ -68,15 +60,13 @@ public static class TerminalTool
private readonly StringBuilder _stdoutBuffer = new();
private readonly StringBuilder _stderrBuffer = new();
- private readonly object _lockObject = new();
-
+ private readonly Lock _lockObject = new();
private bool _isDisposed;
public TerminalSession(string id, string label)
{
Id = id;
Label = label;
-
Process = new Process
{
StartInfo = new ProcessStartInfo
@@ -95,23 +85,19 @@ public static class TerminalTool
Process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
- {
lock (_lockObject)
{
_stdoutBuffer.AppendLine(e.Data);
}
- }
};
Process.ErrorDataReceived += (sender, e) =>
{
if (e.Data != null)
- {
lock (_lockObject)
{
_stderrBuffer.AppendLine(e.Data);
}
- }
};
Process.Start();
@@ -166,10 +152,8 @@ public static class TerminalTool
{
string stdout = _stdoutBuffer.ToString();
string stderr = _stderrBuffer.ToString();
-
_stdoutBuffer.Clear();
_stderrBuffer.Clear();
-
return (stdout, stderr);
}
}
@@ -195,16 +179,13 @@ public static class TerminalTool
{
if (_isDisposed)
return;
-
_isDisposed = true;
-
try
{
if (!Process.HasExited)
{
Process.StandardInput.WriteLine("exit");
Process.StandardInput.Flush();
-
if (!Process.WaitForExit(1000))
Process.Kill();
}
@@ -228,10 +209,8 @@ public static class TerminalTool
{
if (process.HasExited)
return false;
-
if (!AttachConsole((uint)process.Id))
return false;
-
SetConsoleCtrlHandler(null, true);
try
{
@@ -264,11 +243,9 @@ public static class TerminalTool
UseShellExecute = false,
CreateNoWindow = true
});
-
killProcess?.WaitForExit();
return killProcess?.ExitCode == 0;
}
-
return false;
}
catch
@@ -277,7 +254,6 @@ public static class TerminalTool
}
}
- // Windows Console API imports
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
public static extern bool AttachConsole(uint dwProcessId);
@@ -285,7 +261,7 @@ public static class TerminalTool
public static extern bool FreeConsole();
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
- public static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandler? HandlerRoutine, bool Add);
+ public static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandler? handlerRoutine, bool add);
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
public static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);
@@ -298,34 +274,30 @@ public static class TerminalTool
CTRL_LOGOFF_EVENT = 5,
CTRL_SHUTDOWN_EVENT = 6
}
-
public delegate bool ConsoleCtrlHandler(CtrlTypes ctrlType);
}
- //——— Tool Methods ———//
-
- [McpServerTool, Description("""
- List all active terminal sessions.
- Return Format:
- {
- sessions: [ { session_id: string, label: string }, ... ]
- }
+ [McpServerTool, Description(
+ """
+ Lists all currently active persistent terminal sessions being managed.
+ Provides the essential `sessionId` (required for all other operations on a session) and the human-readable `label` for each.
+ Use this to find the `sessionId` of an existing session you want to interact with.
+ Return Format: { sessions: [ { session_id: string, label: string }, ... ] }
""")]
- public static ListSessionsResult ListSessions()
+ public static ListSessionsResult ListTerminalSessions()
{
- var sessions = _sessions.Values.Select(s => new SessionInfo(s.Id, s.Label)).ToList();
+ List sessions = _sessions.Values.Select(s => new SessionInfo(s.Id, s.Label)).ToList();
return new ListSessionsResult(sessions);
}
- [McpServerTool, Description("""
- Create a new terminal session.
- Return Format:
- {
- status: 'success' | 'fail',
- session: { session_id: string, label: string }
- }
+ [McpServerTool, Description(
+ """
+ Creates a new persistent terminal session (a background shell like bash or cmd) and assigns it a unique `sessionId`.
+ This `sessionId` is **essential** and **must be used** in subsequent calls to other TerminalTool functions (like SendInput..., OneOffCommand, GetCachedResult, ExitSession) to target this specific session.
+ The `label` is purely for human reference (e.g., in ListTerminalSessions).
+ Return Format: { status: 'success' | 'fail', session: { session_id: string, label: string } }
""")]
- public static CreateSessionResult CreateSession(
+ public static CreateSessionResult CreateTerminalSession(
[Description("Memorable label for the new session.")] string label
)
{
@@ -333,12 +305,8 @@ public static class TerminalTool
{
string sessionId = Guid.NewGuid().ToString("N");
var session = new TerminalSession(sessionId, label);
-
if (_sessions.TryAdd(sessionId, session))
- {
return new CreateSessionResult("success", new SessionInfo(sessionId, label));
- }
-
session.Dispose();
return new CreateSessionResult("fail", new SessionInfo(string.Empty, string.Empty));
}
@@ -347,15 +315,14 @@ public static class TerminalTool
return new CreateSessionResult("fail", new SessionInfo(string.Empty, string.Empty));
}
}
-
- [McpServerTool, Description("""
- Exit a terminal session. Fails if the session ID does not exist.
- Return Format:
- {
- status: 'success' | 'fail'
- }
+ [McpServerTool, Description(
+ """
+ Terminates and cleans up a specific persistent terminal session identified by its `sessionId`.
+ This effectively closes the background shell process associated with that session.
+ Use this when you are finished working with a specific session. Fails if the `sessionId` is invalid.
+ Return Format: { status: 'success' | 'fail' }
""")]
- public static ExitSessionResult ExitSession(
+ public static ExitSessionResult ExitTerminalSession(
[Description("ID of the session to exit.")] string sessionId
)
{
@@ -364,18 +331,16 @@ public static class TerminalTool
session.Dispose();
return new ExitSessionResult("success");
}
-
return new ExitSessionResult("fail");
}
-
- [McpServerTool, Description("""
- Send a signal to a terminal session (e.g., SIGINT=2).
- Return Format:
- {
- status: 'success' | 'fail'
- }
+ [McpServerTool, Description(
+ """
+ Terminates and cleans up a specific persistent terminal session identified by its `sessionId`.
+ This effectively closes the background shell process associated with that session.
+ Use this when you are finished working with a specific session. Fails if the `sessionId` is invalid.
+ Return Format: { status: 'success' | 'fail' }
""")]
- public static SendSignalResult SendSignal(
+ public static SendSignalResult SendSignalToTerminalSession(
[Description("Session ID to send the signal to.")] string sessionId,
[Description("Signal number to send (e.g., 2 for SIGINT)." )] int signal
)
@@ -385,23 +350,18 @@ public static class TerminalTool
bool success = session.SendSignal(signal);
return new SendSignalResult(success ? "success" : "fail");
}
-
return new SendSignalResult("fail");
}
- [McpServerTool, Description("""
- Send input to a terminal session and wait until the output matches one of the exception regexes or timeout.
- Use cases: interactive prompts (e.g., confirmation prompts, password input).
- Return Format:
- {
- output_by: 'except' | 'timeout',
- matched_except: string | null,
- stdout: string,
- stderr: string
- }
- Note: cached stdout/stderr is cleared after this call.
+ [McpServerTool, Description(
+ """
+ Sends input text to a specific terminal session (identified by `sessionId`) and WAITS for output.
+ It returns ONLY when specific regex patterns (`excepts`) are matched in the session's stdout/stderr OR the `timeout` is reached.
+ **Use Case:** Handling interactive prompts within the shell (e.g., password prompts, 'yes/no' confirmations) where you need to wait for a specific response pattern before proceeding.
+ Clears the session's output buffer upon returning. Compare with `SendInputAndForget` and `OneOffCommand` for different interaction patterns.
+ Return Format: { output_by: 'except' | 'timeout', matched_except: string | null, stdout: string, stderr: string }
""")]
- public static SendInputWithExceptResult SendInputWithExcept(
+ public static SendInputWithExceptResult SendCommandToTerminalSessionWithExcept(
[Description("Session ID to send input to.")] string sessionId,
[Description("Input text to send (without newline)." )] string input,
[Description("Timeout in milliseconds.")] int timeout,
@@ -409,23 +369,16 @@ public static class TerminalTool
)
{
if (!_sessions.TryGetValue(sessionId, out var session))
- {
return new SendInputWithExceptResult("timeout", null, string.Empty, string.Empty);
- }
-
session.SendInput(input);
-
var regexPatterns = excepts.Select(pattern => new Regex(pattern, RegexOptions.Compiled)).ToArray();
-
string matchedPattern = null;
bool matched = false;
var watch = Stopwatch.StartNew();
-
while (watch.ElapsedMilliseconds < timeout && !matched)
{
var (stdout, stderr) = session.GetBuffers();
string combinedOutput = stdout + stderr;
-
for (int i = 0; i < regexPatterns.Length; i++)
{
if (regexPatterns[i].IsMatch(combinedOutput))
@@ -435,17 +388,11 @@ public static class TerminalTool
break;
}
}
-
if (!matched)
- {
Thread.Sleep(10);
- }
}
-
watch.Stop();
-
var (finalStdout, finalStderr) = session.GetAndClearBuffers();
-
return new SendInputWithExceptResult(
matched ? "except" : "timeout",
matchedPattern,
@@ -454,24 +401,23 @@ public static class TerminalTool
);
}
- [McpServerTool, Description("""
- Send input to a terminal session without waiting for output. Output is cached for later retrieval.
- Use cases: long-running or background commands. e.g. ssh
- Return Format:
- {
- status: 'success' | 'fail'
- }
+ [McpServerTool, Description(
+ """
+ Sends a POSIX-style signal to the process running within the specified terminal session (identified by `sessionId`).
+ Common use is sending signal 2 (SIGINT) to simulate Ctrl+C, interrupting a running command within that specific session's shell.
+ Requires the `sessionId` of an active session. Fails if the `sessionId` is invalid or the signal cannot be delivered.
+ Return Format: { status: 'success' | 'fail' }
""")]
- public static SendInputAndForgetResult SendInputAndForget(
+ public static SendInputAndForgetResult SendCommandToTerminalSessionAndForget(
[Description("Session ID to send input to.")] string sessionId,
- [Description("Input text to send (without newline)." )] string input
+ [Description("Input text to send (without newline)." )] string command
)
{
if (_sessions.TryGetValue(sessionId, out var session))
{
try
{
- session.SendInput(input);
+ session.SendInput(command);
return new SendInputAndForgetResult("success");
}
catch
@@ -483,66 +429,45 @@ public static class TerminalTool
return new SendInputAndForgetResult("fail");
}
- [McpServerTool, Description("""
- Execute a one-off command in the specified session and return stdout/stderr when done or timed out.
- Use cases: quick commands that complete shortly (e.g., ls, echo).
- Return Format:
- {
- return_by: 'finish' | 'timeout',
- stdout: string,
- stderr: string
- }
+ [McpServerTool, Description(
+ """
+ Executes a single command line within a specific terminal session (identified by `sessionId`), waits for it to finish, and returns the collected stdout/stderr.
+ This tool manages sending the command and detecting its completion automatically.
+ **Use Case:** Running relatively quick, synchronous commands where you need the output right away (e.g., `ls -l`, `pwd`, `git status`, `echo $VAR`).
+ Compare with `SendInputAndForget` (for background tasks) and `SendInputWithExcept` (for interactive prompts).
+ Return Format: { return_by: 'finish' | 'timeout' | 'error', stdout: string, stderr: string }
""")]
- public static OneOffCommandResult OneOffCommand(
+ public static OneOffCommandResult SendOneOffCommandToTerminalSession(
[Description("Session ID to execute the command in.")] string sessionId,
[Description("Command to execute, including arguments.")] string command,
[Description("Timeout in milliseconds.")] int timeout
)
{
if (!_sessions.TryGetValue(sessionId, out var session))
- {
- return new OneOffCommandResult("timeout", string.Empty, string.Empty);
- }
-
+ return new OneOffCommandResult("error", string.Empty, string.Empty);
session.ClearBuffers();
-
string marker = $"CMD_COMPLETE_{Guid.NewGuid():N}";
-
if (OperatingSystem.IsWindows())
- {
session.SendInput($"{command} & echo {marker}");
- }
else
- {
session.SendInput($"{command}; echo {marker}");
- }
-
bool completed = false;
var watch = Stopwatch.StartNew();
-
while (watch.ElapsedMilliseconds < timeout && !completed)
{
var (stdout, _) = session.GetBuffers();
-
if (stdout.Contains(marker))
- {
completed = true;
- }
else
- {
Thread.Sleep(10);
- }
}
-
watch.Stop();
-
var (finalStdout, finalStderr) = session.GetAndClearBuffers();
-
if (completed)
{
finalStdout = Regex.Replace(finalStdout, $"{marker}\r?\n?$", "");
+ finalStderr = Regex.Replace(finalStderr, $"{marker}\r?\n?$", "");
}
-
return new OneOffCommandResult(
completed ? "finish" : "timeout",
finalStdout,
@@ -550,15 +475,13 @@ public static class TerminalTool
);
}
- [McpServerTool, Description("""
- Get and clear cached stdout/stderr of the specified session.
- Return Format:
- {
- stdout: string,
- stderr: string
- }
+ [McpServerTool, Description(
+ """
+ Retrieves AND clears all accumulated standard output (stdout) and standard error (stderr) that have been buffered within the specified terminal session (identified by `sessionId`) since the last read or clear operation.
+ **Use Case:** Primarily used to collect the output from commands previously started with `SendInputAndForget`. Call this periodically or when you expect a background task might have produced output.
+ Return Format: { stdout: string, stderr: string }
""")]
- public static GetCachedResultResult GetCachedResult(
+ public static GetCachedResultResult GetTerminalSessionCachedResult(
[Description("Session ID to retrieve output from.")] string sessionId
)
{
@@ -567,16 +490,14 @@ public static class TerminalTool
var (stdout, stderr) = session.GetAndClearBuffers();
return new GetCachedResultResult(stdout, stderr);
}
-
return new GetCachedResultResult(string.Empty, string.Empty);
}
- [McpServerTool, Description("""
- Clear cached stdout/stderr of the specified session.
- Return Format:
- {
- status: 'success' | 'fail'
- }
+ [McpServerTool, Description(
+ """
+ Discards (clears) any accumulated stdout and stderr buffered within the specified terminal session (identified by `sessionId`) without returning the content.
+ **Use Case:** Useful if you want to ignore any previous output before running a new command or checking for fresh output, particularly if using `SendInputAndForget`.
+ Return Format: { status: 'success' | 'fail' }
""")]
public static ClearResult Clear(
[Description("Session ID to clear cache for.")] string sessionId
@@ -587,7 +508,9 @@ public static class TerminalTool
session.ClearBuffers();
return new ClearResult("success");
}
-
return new ClearResult("fail");
}
+
}
+
+
diff --git a/appsettings.json b/appsettings.json
new file mode 100644
index 0000000..909070d
--- /dev/null
+++ b/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Trace"
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/entrypoint.sh b/entrypoint.sh
new file mode 100644
index 0000000..120598e
--- /dev/null
+++ b/entrypoint.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env sh
+set -e
+if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then
+ ssh-keygen -A
+fi
+/usr/sbin/sshd
+
+exec dotnet Alchegos.MCP.dll
diff --git a/playground.sh b/playground.sh
new file mode 100644
index 0000000..677db01
--- /dev/null
+++ b/playground.sh
@@ -0,0 +1,141 @@
+#!/bin/sh
+# ssh_passwordless_manager.sh
+# Manage passwordless SSH hosts: list, add, delete, edit, query.
+
+CONFIG_FILE="$HOME/.ssh_playground_hosts"
+
+# Ensure config file exists
+touch "$CONFIG_FILE"
+
+# Print usage information
+usage() {
+ cat < [options]
+
+Commands:
+ list List all hosts configured for SSH connection
+ add [pub_key]
+ Add a new host; installs public key and saves config
+ delete Remove a host; deletes key from remote and config
+ edit [new_pub_key]
+ Replace the key for a given host
+ query Show the configuration for a given host
+ help Show this help message
+EOF
+ exit 1
+}
+
+# Load entry by alias
+load_entry() {
+ grep "^$1|" "$CONFIG_FILE"
+}
+
+# List hosts
+list_hosts() {
+ if [ ! -s "$CONFIG_FILE" ]; then
+ echo "No hosts configured."
+ exit 0
+ fi
+ printf "%-15s %-25s %s\n" "ALIAS" "USER@HOST" "PRIVATE_KEY"
+ echo "---------------------------------------------------------------"
+ while IFS='|' read -r alias userhost privkey pubkey; do
+ printf "%-15s %-25s %s\n" "$alias" "$userhost" "$privkey"
+ done < "$CONFIG_FILE"
+}
+
+# Add host
+add_host() {
+ [ $# -lt 3 ] && echo "add requires alias, user@host, private key" && usage
+ alias="$1"; userhost="$2"; privkey="$3"; pubkey="$4"
+ [ -z "$pubkey" ] && pubkey="${privkey}.pub"
+
+ if load_entry "$alias" >/dev/null; then
+ echo "Alias '$alias' already exists." && exit 1
+ fi
+
+ # Copy public key to remote
+ echo "Installing public key to $userhost..."
+ ssh-copy-id -i "$pubkey" "$userhost" || { echo "Failed to install key."; exit 1; }
+
+ # Save to config
+ echo "$alias|$userhost|$privkey|$pubkey" >> "$CONFIG_FILE"
+ echo "Host '$alias' added."
+}
+
+# Delete host
+delete_host() {
+ [ $# -lt 1 ] && echo "delete requires alias" && usage
+ alias="$1"
+ entry=$(load_entry "$alias")
+ [ -z "$entry" ] && echo "Alias '$alias' not found." && exit 1
+
+ IFS='|' read -r _ userhost privkey pubkey < ~/.ssh/authorized_keys.tmp && \
+ mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys" || echo "Warning: could not remove key on remote."
+
+ # Remove from config
+ grep -v "^$alias|" "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
+ echo "Host '$alias' deleted."
+}
+
+# Edit host key
+edit_host() {
+ [ $# -lt 2 ] && echo "edit requires alias and new private key" && usage
+ alias="$1"; newpriv="$2"; newpub="$3"
+ [ -z "$newpub" ] && newpub="${newpriv}.pub"
+ entry=$(load_entry "$alias")
+ [ -z "$entry" ] && echo "Alias '$alias' not found." && exit 1
+
+ IFS='|' read -r _ userhost oldpriv oldpub < ~/.ssh/authorized_keys.tmp && mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys" || echo "Warning: could not remove old key."
+
+ # Install new key
+ echo "Installing new public key to $userhost..."
+ ssh-copy-id -i "$newpub" "$userhost" || { echo "Failed to install new key."; exit 1; }
+
+ # Update config
+ grep -v "^$alias|" "$CONFIG_FILE" > "$CONFIG_FILE.tmp"
+ echo "$alias|$userhost|$newpriv|$newpub" >> "$CONFIG_FILE.tmp"
+ mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
+ echo "Host '$alias' updated."
+}
+
+# Query host
+query_host() {
+ [ $# -lt 1 ] && echo "query requires alias" && usage
+ alias="$1"
+ entry=$(load_entry "$alias")
+ [ -z "$entry" ] && echo "Alias '$alias' not found." && exit 1
+ IFS='|' read -r _ userhost privkey pubkey <