add: hci
This commit is contained in:
@@ -15,5 +15,9 @@
|
|||||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.1.0-preview.11" />
|
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.1.0-preview.11" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="NuGet.Config" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
13
Dockerfile
13
Dockerfile
@@ -5,6 +5,7 @@ WORKDIR /app
|
|||||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
COPY NuGet.Config ./
|
||||||
COPY ["*.sln", "."]
|
COPY ["*.sln", "."]
|
||||||
COPY ["Alchegos.MCP.csproj", "./"]
|
COPY ["Alchegos.MCP.csproj", "./"]
|
||||||
RUN dotnet restore "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
|
FROM base AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=publish /app/publish .
|
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"]
|
||||||
|
|||||||
51
Middleware/RequestBodyLoggingMiddleware.cs
Normal file
51
Middleware/RequestBodyLoggingMiddleware.cs
Normal file
@@ -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<RequestBodyLoggingMiddleware> 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<Alchegos.MCP.Middleware.RequestBodyLoggingMiddleware>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
NuGet.Config
Normal file
7
NuGet.Config
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||||
|
<add key="hangman-lab" value="https://git.hangman-lab.top/api/packages/hzhang/nuget/index.json" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
24
Program.cs
24
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);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
const int BIND_PORT = 5050;
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddMcpServer()
|
.AddMcpServer()
|
||||||
.WithHttpTransport()
|
.WithHttpTransport()
|
||||||
.WithToolsFromAssembly();
|
.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 app = builder.Build();
|
||||||
|
// var logger = app.Logger;
|
||||||
|
//app.UseHttpLogging();
|
||||||
|
app.UseRequestBodyLogging();
|
||||||
|
|
||||||
app.MapMcp();
|
app.MapMcp();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public static class EchoTool
|
|
||||||
{
|
|
||||||
[McpServerTool, Description("Echoes the message back to the client.")]
|
|
||||||
public static string Echo(string message) => $"hello {message}";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
using Alchegos.Core.Services.Gitea;
|
||||||
|
|
||||||
|
|
||||||
namespace Alchegos.MCP.Tools;
|
namespace Alchegos.MCP.Tools;
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public static class GiteaTool
|
public static class GiteaTool
|
||||||
{
|
{
|
||||||
|
public static string Login(string a)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetGiteaRepoList(string apiKey)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,60 +6,52 @@ using System.Text.RegularExpressions;
|
|||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
namespace Alchegos.MCP.Tools;
|
namespace Alchegos.MCP.Tools;
|
||||||
|
[McpServerToolType, Description("""
|
||||||
[McpServerToolType, Description("Manage multiple persistent terminal sessions, enabling creation, closure, command execution, and output retrieval.")]
|
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
|
public static class TerminalTool
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<string, TerminalSession> _sessions = new();
|
private static readonly ConcurrentDictionary<string, TerminalSession> _sessions = new();
|
||||||
|
|
||||||
public record SessionInfo(
|
public record SessionInfo(
|
||||||
[property: Description("Unique identifier for the session.")] string SessionId,
|
[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.")] string Label
|
[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(
|
public record ListSessionsResult(
|
||||||
[property: Description("List of all active sessions with their IDs and labels.")] List<SessionInfo> 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<SessionInfo> Sessions
|
||||||
);
|
);
|
||||||
|
|
||||||
public record CreateSessionResult(
|
public record CreateSessionResult(
|
||||||
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status,
|
[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
|
[property: Description("Details of the created session if successful; otherwise, with empty fields.")] SessionInfo Session
|
||||||
);
|
);
|
||||||
|
|
||||||
public record ExitSessionResult(
|
public record ExitSessionResult(
|
||||||
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
|
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
|
||||||
);
|
);
|
||||||
|
|
||||||
public record SendSignalResult(
|
public record SendSignalResult(
|
||||||
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
|
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
|
||||||
);
|
);
|
||||||
|
|
||||||
public record SendInputWithExceptResult(
|
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("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("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 output from the session after sending input.")] string Stdout,
|
||||||
[property: Description("Accumulated standard error from the session after sending input.")] string Stderr
|
[property: Description("Accumulated standard error from the session after sending input.")] string Stderr
|
||||||
);
|
);
|
||||||
|
|
||||||
public record SendInputAndForgetResult(
|
public record SendInputAndForgetResult(
|
||||||
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
|
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
|
||||||
);
|
);
|
||||||
|
|
||||||
public record OneOffCommandResult(
|
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 output produced by the command.")] string Stdout,
|
||||||
[property: Description("Standard error produced by the command.")] string Stderr
|
[property: Description("Standard error produced by the command.")] string Stderr
|
||||||
);
|
);
|
||||||
|
|
||||||
public record GetCachedResultResult(
|
public record GetCachedResultResult(
|
||||||
[property: Description("Cached standard output of the session.")] string Stdout,
|
[property: Description("Cached standard output of the session.")] string Stdout,
|
||||||
[property: Description("Cached standard error of the session.")] string Stderr
|
[property: Description("Cached standard error of the session.")] string Stderr
|
||||||
);
|
);
|
||||||
|
|
||||||
public record ClearResult(
|
public record ClearResult(
|
||||||
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
|
[property: Description("Status of the operation: 'success' or 'fail'.")] string Status
|
||||||
);
|
);
|
||||||
|
|
||||||
private class TerminalSession : IDisposable
|
private class TerminalSession : IDisposable
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
@@ -68,15 +60,13 @@ public static class TerminalTool
|
|||||||
|
|
||||||
private readonly StringBuilder _stdoutBuffer = new();
|
private readonly StringBuilder _stdoutBuffer = new();
|
||||||
private readonly StringBuilder _stderrBuffer = new();
|
private readonly StringBuilder _stderrBuffer = new();
|
||||||
private readonly object _lockObject = new();
|
private readonly Lock _lockObject = new();
|
||||||
|
|
||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
|
|
||||||
public TerminalSession(string id, string label)
|
public TerminalSession(string id, string label)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Label = label;
|
Label = label;
|
||||||
|
|
||||||
Process = new Process
|
Process = new Process
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
StartInfo = new ProcessStartInfo
|
||||||
@@ -95,23 +85,19 @@ public static class TerminalTool
|
|||||||
Process.OutputDataReceived += (sender, e) =>
|
Process.OutputDataReceived += (sender, e) =>
|
||||||
{
|
{
|
||||||
if (e.Data != null)
|
if (e.Data != null)
|
||||||
{
|
|
||||||
lock (_lockObject)
|
lock (_lockObject)
|
||||||
{
|
{
|
||||||
_stdoutBuffer.AppendLine(e.Data);
|
_stdoutBuffer.AppendLine(e.Data);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Process.ErrorDataReceived += (sender, e) =>
|
Process.ErrorDataReceived += (sender, e) =>
|
||||||
{
|
{
|
||||||
if (e.Data != null)
|
if (e.Data != null)
|
||||||
{
|
|
||||||
lock (_lockObject)
|
lock (_lockObject)
|
||||||
{
|
{
|
||||||
_stderrBuffer.AppendLine(e.Data);
|
_stderrBuffer.AppendLine(e.Data);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Process.Start();
|
Process.Start();
|
||||||
@@ -166,10 +152,8 @@ public static class TerminalTool
|
|||||||
{
|
{
|
||||||
string stdout = _stdoutBuffer.ToString();
|
string stdout = _stdoutBuffer.ToString();
|
||||||
string stderr = _stderrBuffer.ToString();
|
string stderr = _stderrBuffer.ToString();
|
||||||
|
|
||||||
_stdoutBuffer.Clear();
|
_stdoutBuffer.Clear();
|
||||||
_stderrBuffer.Clear();
|
_stderrBuffer.Clear();
|
||||||
|
|
||||||
return (stdout, stderr);
|
return (stdout, stderr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,16 +179,13 @@ public static class TerminalTool
|
|||||||
{
|
{
|
||||||
if (_isDisposed)
|
if (_isDisposed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!Process.HasExited)
|
if (!Process.HasExited)
|
||||||
{
|
{
|
||||||
Process.StandardInput.WriteLine("exit");
|
Process.StandardInput.WriteLine("exit");
|
||||||
Process.StandardInput.Flush();
|
Process.StandardInput.Flush();
|
||||||
|
|
||||||
if (!Process.WaitForExit(1000))
|
if (!Process.WaitForExit(1000))
|
||||||
Process.Kill();
|
Process.Kill();
|
||||||
}
|
}
|
||||||
@@ -228,10 +209,8 @@ public static class TerminalTool
|
|||||||
{
|
{
|
||||||
if (process.HasExited)
|
if (process.HasExited)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!AttachConsole((uint)process.Id))
|
if (!AttachConsole((uint)process.Id))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
SetConsoleCtrlHandler(null, true);
|
SetConsoleCtrlHandler(null, true);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -264,11 +243,9 @@ public static class TerminalTool
|
|||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
CreateNoWindow = true
|
CreateNoWindow = true
|
||||||
});
|
});
|
||||||
|
|
||||||
killProcess?.WaitForExit();
|
killProcess?.WaitForExit();
|
||||||
return killProcess?.ExitCode == 0;
|
return killProcess?.ExitCode == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -277,7 +254,6 @@ public static class TerminalTool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows Console API imports
|
|
||||||
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||||||
public static extern bool AttachConsole(uint dwProcessId);
|
public static extern bool AttachConsole(uint dwProcessId);
|
||||||
|
|
||||||
@@ -285,7 +261,7 @@ public static class TerminalTool
|
|||||||
public static extern bool FreeConsole();
|
public static extern bool FreeConsole();
|
||||||
|
|
||||||
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
[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")]
|
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||||||
public static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);
|
public static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);
|
||||||
@@ -298,34 +274,30 @@ public static class TerminalTool
|
|||||||
CTRL_LOGOFF_EVENT = 5,
|
CTRL_LOGOFF_EVENT = 5,
|
||||||
CTRL_SHUTDOWN_EVENT = 6
|
CTRL_SHUTDOWN_EVENT = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
public delegate bool ConsoleCtrlHandler(CtrlTypes ctrlType);
|
public delegate bool ConsoleCtrlHandler(CtrlTypes ctrlType);
|
||||||
}
|
}
|
||||||
|
|
||||||
//——— Tool Methods ———//
|
[McpServerTool, Description(
|
||||||
|
"""
|
||||||
[McpServerTool, Description("""
|
Lists all currently active persistent terminal sessions being managed.
|
||||||
List all active terminal sessions.
|
Provides the essential `sessionId` (required for all other operations on a session) and the human-readable `label` for each.
|
||||||
Return Format:
|
Use this to find the `sessionId` of an existing session you want to interact with.
|
||||||
{
|
Return Format: { sessions: [ { session_id: string, label: string }, ... ] }
|
||||||
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<SessionInfo> sessions = _sessions.Values.Select(s => new SessionInfo(s.Id, s.Label)).ToList();
|
||||||
return new ListSessionsResult(sessions);
|
return new ListSessionsResult(sessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("""
|
[McpServerTool, Description(
|
||||||
Create a new terminal session.
|
"""
|
||||||
Return Format:
|
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.
|
||||||
status: 'success' | 'fail',
|
The `label` is purely for human reference (e.g., in ListTerminalSessions).
|
||||||
session: { session_id: string, label: string }
|
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
|
[Description("Memorable label for the new session.")] string label
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -333,12 +305,8 @@ public static class TerminalTool
|
|||||||
{
|
{
|
||||||
string sessionId = Guid.NewGuid().ToString("N");
|
string sessionId = Guid.NewGuid().ToString("N");
|
||||||
var session = new TerminalSession(sessionId, label);
|
var session = new TerminalSession(sessionId, label);
|
||||||
|
|
||||||
if (_sessions.TryAdd(sessionId, session))
|
if (_sessions.TryAdd(sessionId, session))
|
||||||
{
|
|
||||||
return new CreateSessionResult("success", new SessionInfo(sessionId, label));
|
return new CreateSessionResult("success", new SessionInfo(sessionId, label));
|
||||||
}
|
|
||||||
|
|
||||||
session.Dispose();
|
session.Dispose();
|
||||||
return new CreateSessionResult("fail", new SessionInfo(string.Empty, string.Empty));
|
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));
|
return new CreateSessionResult("fail", new SessionInfo(string.Empty, string.Empty));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
[McpServerTool, Description(
|
||||||
[McpServerTool, Description("""
|
"""
|
||||||
Exit a terminal session. Fails if the session ID does not exist.
|
Terminates and cleans up a specific persistent terminal session identified by its `sessionId`.
|
||||||
Return Format:
|
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.
|
||||||
status: 'success' | 'fail'
|
Return Format: { status: 'success' | 'fail' }
|
||||||
}
|
|
||||||
""")]
|
""")]
|
||||||
public static ExitSessionResult ExitSession(
|
public static ExitSessionResult ExitTerminalSession(
|
||||||
[Description("ID of the session to exit.")] string sessionId
|
[Description("ID of the session to exit.")] string sessionId
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -364,18 +331,16 @@ public static class TerminalTool
|
|||||||
session.Dispose();
|
session.Dispose();
|
||||||
return new ExitSessionResult("success");
|
return new ExitSessionResult("success");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ExitSessionResult("fail");
|
return new ExitSessionResult("fail");
|
||||||
}
|
}
|
||||||
|
[McpServerTool, Description(
|
||||||
[McpServerTool, Description("""
|
"""
|
||||||
Send a signal to a terminal session (e.g., SIGINT=2).
|
Terminates and cleans up a specific persistent terminal session identified by its `sessionId`.
|
||||||
Return Format:
|
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.
|
||||||
status: 'success' | 'fail'
|
Return Format: { status: 'success' | 'fail' }
|
||||||
}
|
|
||||||
""")]
|
""")]
|
||||||
public static SendSignalResult SendSignal(
|
public static SendSignalResult SendSignalToTerminalSession(
|
||||||
[Description("Session ID to send the signal to.")] string sessionId,
|
[Description("Session ID to send the signal to.")] string sessionId,
|
||||||
[Description("Signal number to send (e.g., 2 for SIGINT)." )] int signal
|
[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);
|
bool success = session.SendSignal(signal);
|
||||||
return new SendSignalResult(success ? "success" : "fail");
|
return new SendSignalResult(success ? "success" : "fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SendSignalResult("fail");
|
return new SendSignalResult("fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("""
|
[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).
|
Sends input text to a specific terminal session (identified by `sessionId`) and WAITS for output.
|
||||||
Return Format:
|
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.
|
||||||
output_by: 'except' | 'timeout',
|
Clears the session's output buffer upon returning. Compare with `SendInputAndForget` and `OneOffCommand` for different interaction patterns.
|
||||||
matched_except: string | null,
|
Return Format: { output_by: 'except' | 'timeout', matched_except: string | null, stdout: string, stderr: string }
|
||||||
stdout: string,
|
|
||||||
stderr: string
|
|
||||||
}
|
|
||||||
Note: cached stdout/stderr is cleared after this call.
|
|
||||||
""")]
|
""")]
|
||||||
public static SendInputWithExceptResult SendInputWithExcept(
|
public static SendInputWithExceptResult SendCommandToTerminalSessionWithExcept(
|
||||||
[Description("Session ID to send input to.")] string sessionId,
|
[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 input,
|
||||||
[Description("Timeout in milliseconds.")] int timeout,
|
[Description("Timeout in milliseconds.")] int timeout,
|
||||||
@@ -409,23 +369,16 @@ public static class TerminalTool
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (!_sessions.TryGetValue(sessionId, out var session))
|
if (!_sessions.TryGetValue(sessionId, out var session))
|
||||||
{
|
|
||||||
return new SendInputWithExceptResult("timeout", null, string.Empty, string.Empty);
|
return new SendInputWithExceptResult("timeout", null, string.Empty, string.Empty);
|
||||||
}
|
|
||||||
|
|
||||||
session.SendInput(input);
|
session.SendInput(input);
|
||||||
|
|
||||||
var regexPatterns = excepts.Select(pattern => new Regex(pattern, RegexOptions.Compiled)).ToArray();
|
var regexPatterns = excepts.Select(pattern => new Regex(pattern, RegexOptions.Compiled)).ToArray();
|
||||||
|
|
||||||
string matchedPattern = null;
|
string matchedPattern = null;
|
||||||
bool matched = false;
|
bool matched = false;
|
||||||
var watch = Stopwatch.StartNew();
|
var watch = Stopwatch.StartNew();
|
||||||
|
|
||||||
while (watch.ElapsedMilliseconds < timeout && !matched)
|
while (watch.ElapsedMilliseconds < timeout && !matched)
|
||||||
{
|
{
|
||||||
var (stdout, stderr) = session.GetBuffers();
|
var (stdout, stderr) = session.GetBuffers();
|
||||||
string combinedOutput = stdout + stderr;
|
string combinedOutput = stdout + stderr;
|
||||||
|
|
||||||
for (int i = 0; i < regexPatterns.Length; i++)
|
for (int i = 0; i < regexPatterns.Length; i++)
|
||||||
{
|
{
|
||||||
if (regexPatterns[i].IsMatch(combinedOutput))
|
if (regexPatterns[i].IsMatch(combinedOutput))
|
||||||
@@ -435,17 +388,11 @@ public static class TerminalTool
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!matched)
|
if (!matched)
|
||||||
{
|
|
||||||
Thread.Sleep(10);
|
Thread.Sleep(10);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
watch.Stop();
|
watch.Stop();
|
||||||
|
|
||||||
var (finalStdout, finalStderr) = session.GetAndClearBuffers();
|
var (finalStdout, finalStderr) = session.GetAndClearBuffers();
|
||||||
|
|
||||||
return new SendInputWithExceptResult(
|
return new SendInputWithExceptResult(
|
||||||
matched ? "except" : "timeout",
|
matched ? "except" : "timeout",
|
||||||
matchedPattern,
|
matchedPattern,
|
||||||
@@ -454,24 +401,23 @@ public static class TerminalTool
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("""
|
[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
|
Sends a POSIX-style signal to the process running within the specified terminal session (identified by `sessionId`).
|
||||||
Return Format:
|
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.
|
||||||
status: 'success' | 'fail'
|
Return Format: { status: 'success' | 'fail' }
|
||||||
}
|
|
||||||
""")]
|
""")]
|
||||||
public static SendInputAndForgetResult SendInputAndForget(
|
public static SendInputAndForgetResult SendCommandToTerminalSessionAndForget(
|
||||||
[Description("Session ID to send input to.")] string sessionId,
|
[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))
|
if (_sessions.TryGetValue(sessionId, out var session))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
session.SendInput(input);
|
session.SendInput(command);
|
||||||
return new SendInputAndForgetResult("success");
|
return new SendInputAndForgetResult("success");
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -483,66 +429,45 @@ public static class TerminalTool
|
|||||||
return new SendInputAndForgetResult("fail");
|
return new SendInputAndForgetResult("fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("""
|
[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).
|
Executes a single command line within a specific terminal session (identified by `sessionId`), waits for it to finish, and returns the collected stdout/stderr.
|
||||||
Return Format:
|
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`).
|
||||||
return_by: 'finish' | 'timeout',
|
Compare with `SendInputAndForget` (for background tasks) and `SendInputWithExcept` (for interactive prompts).
|
||||||
stdout: string,
|
Return Format: { return_by: 'finish' | 'timeout' | 'error', stdout: string, stderr: string }
|
||||||
stderr: string
|
|
||||||
}
|
|
||||||
""")]
|
""")]
|
||||||
public static OneOffCommandResult OneOffCommand(
|
public static OneOffCommandResult SendOneOffCommandToTerminalSession(
|
||||||
[Description("Session ID to execute the command in.")] string sessionId,
|
[Description("Session ID to execute the command in.")] string sessionId,
|
||||||
[Description("Command to execute, including arguments.")] string command,
|
[Description("Command to execute, including arguments.")] string command,
|
||||||
[Description("Timeout in milliseconds.")] int timeout
|
[Description("Timeout in milliseconds.")] int timeout
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (!_sessions.TryGetValue(sessionId, out var session))
|
if (!_sessions.TryGetValue(sessionId, out var session))
|
||||||
{
|
return new OneOffCommandResult("error", string.Empty, string.Empty);
|
||||||
return new OneOffCommandResult("timeout", string.Empty, string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
session.ClearBuffers();
|
session.ClearBuffers();
|
||||||
|
|
||||||
string marker = $"CMD_COMPLETE_{Guid.NewGuid():N}";
|
string marker = $"CMD_COMPLETE_{Guid.NewGuid():N}";
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
|
||||||
session.SendInput($"{command} & echo {marker}");
|
session.SendInput($"{command} & echo {marker}");
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
session.SendInput($"{command}; echo {marker}");
|
session.SendInput($"{command}; echo {marker}");
|
||||||
}
|
|
||||||
|
|
||||||
bool completed = false;
|
bool completed = false;
|
||||||
var watch = Stopwatch.StartNew();
|
var watch = Stopwatch.StartNew();
|
||||||
|
|
||||||
while (watch.ElapsedMilliseconds < timeout && !completed)
|
while (watch.ElapsedMilliseconds < timeout && !completed)
|
||||||
{
|
{
|
||||||
var (stdout, _) = session.GetBuffers();
|
var (stdout, _) = session.GetBuffers();
|
||||||
|
|
||||||
if (stdout.Contains(marker))
|
if (stdout.Contains(marker))
|
||||||
{
|
|
||||||
completed = true;
|
completed = true;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
Thread.Sleep(10);
|
Thread.Sleep(10);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
watch.Stop();
|
watch.Stop();
|
||||||
|
|
||||||
var (finalStdout, finalStderr) = session.GetAndClearBuffers();
|
var (finalStdout, finalStderr) = session.GetAndClearBuffers();
|
||||||
|
|
||||||
if (completed)
|
if (completed)
|
||||||
{
|
{
|
||||||
finalStdout = Regex.Replace(finalStdout, $"{marker}\r?\n?$", "");
|
finalStdout = Regex.Replace(finalStdout, $"{marker}\r?\n?$", "");
|
||||||
|
finalStderr = Regex.Replace(finalStderr, $"{marker}\r?\n?$", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OneOffCommandResult(
|
return new OneOffCommandResult(
|
||||||
completed ? "finish" : "timeout",
|
completed ? "finish" : "timeout",
|
||||||
finalStdout,
|
finalStdout,
|
||||||
@@ -550,15 +475,13 @@ public static class TerminalTool
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("""
|
[McpServerTool, Description(
|
||||||
Get and clear cached stdout/stderr of the specified session.
|
"""
|
||||||
Return Format:
|
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.
|
||||||
stdout: string,
|
Return Format: { stdout: string, stderr: string }
|
||||||
stderr: string
|
|
||||||
}
|
|
||||||
""")]
|
""")]
|
||||||
public static GetCachedResultResult GetCachedResult(
|
public static GetCachedResultResult GetTerminalSessionCachedResult(
|
||||||
[Description("Session ID to retrieve output from.")] string sessionId
|
[Description("Session ID to retrieve output from.")] string sessionId
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -567,16 +490,14 @@ public static class TerminalTool
|
|||||||
var (stdout, stderr) = session.GetAndClearBuffers();
|
var (stdout, stderr) = session.GetAndClearBuffers();
|
||||||
return new GetCachedResultResult(stdout, stderr);
|
return new GetCachedResultResult(stdout, stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GetCachedResultResult(string.Empty, string.Empty);
|
return new GetCachedResultResult(string.Empty, string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("""
|
[McpServerTool, Description(
|
||||||
Clear cached stdout/stderr of the specified session.
|
"""
|
||||||
Return Format:
|
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`.
|
||||||
status: 'success' | 'fail'
|
Return Format: { status: 'success' | 'fail' }
|
||||||
}
|
|
||||||
""")]
|
""")]
|
||||||
public static ClearResult Clear(
|
public static ClearResult Clear(
|
||||||
[Description("Session ID to clear cache for.")] string sessionId
|
[Description("Session ID to clear cache for.")] string sessionId
|
||||||
@@ -587,7 +508,9 @@ public static class TerminalTool
|
|||||||
session.ClearBuffers();
|
session.ClearBuffers();
|
||||||
return new ClearResult("success");
|
return new ClearResult("success");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ClearResult("fail");
|
return new ClearResult("fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
appsettings.json
Normal file
9
appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Trace"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
entrypoint.sh
Normal file
8
entrypoint.sh
Normal file
@@ -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
|
||||||
141
playground.sh
Normal file
141
playground.sh
Normal file
@@ -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 <<EOF
|
||||||
|
Usage: $(basename "$0") <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
list List all hosts configured for SSH connection
|
||||||
|
add <alias> <user@host> <priv_key> [pub_key]
|
||||||
|
Add a new host; installs public key and saves config
|
||||||
|
delete <alias> Remove a host; deletes key from remote and config
|
||||||
|
edit <alias> <new_priv_key> [new_pub_key]
|
||||||
|
Replace the key for a given host
|
||||||
|
query <alias> 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 <<EOF
|
||||||
|
$entry
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Read public key content
|
||||||
|
pubcontent=$(sed -n '1p' "$pubkey")
|
||||||
|
|
||||||
|
# Remove key from remote authorized_keys
|
||||||
|
echo "Removing public key from $userhost..."
|
||||||
|
ssh "$userhost" "mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
|
||||||
|
grep -v '$pubcontent' ~/.ssh/authorized_keys > ~/.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 <<EOF
|
||||||
|
$entry
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Remove old key
|
||||||
|
oldcontent=$(sed -n '1p' "$oldpub")
|
||||||
|
echo "Removing old key from $userhost..."
|
||||||
|
ssh "$userhost" "grep -v '$oldcontent' ~/.ssh/authorized_keys > ~/.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 <<EOF
|
||||||
|
$entry
|
||||||
|
EOF
|
||||||
|
echo "Alias : $alias"
|
||||||
|
echo "Host : $userhost"
|
||||||
|
echo "Priv key : $privkey"
|
||||||
|
echo "Pub key : $pubkey"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main dispatch
|
||||||
|
case "$1" in
|
||||||
|
list) list_hosts ;;
|
||||||
|
add) shift; add_host "$@" ;;
|
||||||
|
delete) shift; delete_host "$@" ;;
|
||||||
|
edit) shift; edit_host "$@" ;;
|
||||||
|
query) shift; query_host "$@" ;;
|
||||||
|
help|*) usage ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user