add: terminal tool
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
@@ -8,7 +9,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3"/>
|
<PackageReference Include="Alchegos.Core" Version="0.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.3.25171.5" />
|
||||||
|
<PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.11" />
|
||||||
|
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.1.0-preview.11" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
@Alchegos.MCP_HostAddress = http://localhost:5031
|
|
||||||
|
|
||||||
GET {{Alchegos.MCP_HostAddress}}/weatherforecast/
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
###
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||||
USER $APP_UID
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 8080
|
|
||||||
EXPOSE 8081
|
|
||||||
|
|
||||||
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 ["*.sln", "."]
|
||||||
COPY ["Alchegos.MCP.csproj", "./"]
|
COPY ["Alchegos.MCP.csproj", "./"]
|
||||||
RUN dotnet restore "Alchegos.MCP.csproj"
|
RUN dotnet restore "Alchegos.MCP.csproj"
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
50
Program.cs
50
Program.cs
@@ -1,41 +1,29 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
// Add services to the container.
|
builder.Services
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
.AddMcpServer()
|
||||||
builder.Services.AddOpenApi();
|
.WithHttpTransport()
|
||||||
|
.WithToolsFromAssembly();
|
||||||
|
builder.WebHost.ConfigureKestrel(options => options.ListenAnyIP(5050));
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.MapOpenApi();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.MapMcp();
|
||||||
|
|
||||||
var summaries = new[]
|
|
||||||
{
|
|
||||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
|
||||||
};
|
|
||||||
|
|
||||||
app.MapGet("/weatherforecast", () =>
|
|
||||||
{
|
|
||||||
var forecast = Enumerable.Range(1, 5).Select(index =>
|
|
||||||
new WeatherForecast
|
|
||||||
(
|
|
||||||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
|
||||||
Random.Shared.Next(-20, 55),
|
|
||||||
summaries[Random.Shared.Next(summaries.Length)]
|
|
||||||
))
|
|
||||||
.ToArray();
|
|
||||||
return forecast;
|
|
||||||
})
|
|
||||||
.WithName("GetWeatherForecast");
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
[McpServerToolType]
|
||||||
|
public static class EchoTool
|
||||||
{
|
{
|
||||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
[McpServerTool, Description("Echoes the message back to the client.")]
|
||||||
|
public static string Echo(string message) => $"hello {message}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
|
||||||
"profiles": {
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": false,
|
|
||||||
"applicationUrl": "http://localhost:5031",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": false,
|
|
||||||
"applicationUrl": "https://localhost:7222;http://localhost:5031",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
Tools/GiteaTool.cs
Normal file
8
Tools/GiteaTool.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
namespace Alchegos.MCP.Tools;
|
||||||
|
[McpServerToolType]
|
||||||
|
public static class GiteaTool
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
593
Tools/TerminalTool.cs
Normal file
593
Tools/TerminalTool.cs
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
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.")]
|
||||||
|
public static class TerminalTool
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, TerminalSession> _sessions = new();
|
||||||
|
|
||||||
|
public record SessionInfo(
|
||||||
|
[property: Description("Unique identifier for the session.")] string SessionId,
|
||||||
|
[property: Description("Human-readable label for the session.")] string Label
|
||||||
|
);
|
||||||
|
|
||||||
|
public record ListSessionsResult(
|
||||||
|
[property: Description("List of all active sessions with their IDs and labels.")] List<SessionInfo> 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("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; }
|
||||||
|
public string Label { get; }
|
||||||
|
public Process Process { get; }
|
||||||
|
|
||||||
|
private readonly StringBuilder _stdoutBuffer = new();
|
||||||
|
private readonly StringBuilder _stderrBuffer = new();
|
||||||
|
private readonly object _lockObject = new();
|
||||||
|
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
public TerminalSession(string id, string label)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Label = label;
|
||||||
|
|
||||||
|
Process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = GetShellPath(),
|
||||||
|
Arguments = GetShellArguments(),
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
},
|
||||||
|
EnableRaisingEvents = true
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
Process.BeginOutputReadLine();
|
||||||
|
Process.BeginErrorReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetShellPath()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
return "cmd.exe";
|
||||||
|
return "/bin/bash";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetShellArguments()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
return "/q";
|
||||||
|
return "-i";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendInput(string input)
|
||||||
|
{
|
||||||
|
if (!Process.HasExited)
|
||||||
|
{
|
||||||
|
Process.StandardInput.WriteLine(input);
|
||||||
|
Process.StandardInput.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SendSignal(int signal)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
if (signal == 2)
|
||||||
|
return NativeMethods.SendCtrlC(Process);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return NativeMethods.SendSignal(Process.Id, signal);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (string stdout, string stderr) GetAndClearBuffers()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
string stdout = _stdoutBuffer.ToString();
|
||||||
|
string stderr = _stderrBuffer.ToString();
|
||||||
|
|
||||||
|
_stdoutBuffer.Clear();
|
||||||
|
_stderrBuffer.Clear();
|
||||||
|
|
||||||
|
return (stdout, stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (string stdout, string stderr) GetBuffers()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return (_stdoutBuffer.ToString(), _stderrBuffer.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearBuffers()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_stdoutBuffer.Clear();
|
||||||
|
_stderrBuffer.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Process.HasExited)
|
||||||
|
{
|
||||||
|
Process.StandardInput.WriteLine("exit");
|
||||||
|
Process.StandardInput.Flush();
|
||||||
|
|
||||||
|
if (!Process.WaitForExit(1000))
|
||||||
|
Process.Kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best effort only
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Process.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NativeMethods
|
||||||
|
{
|
||||||
|
public static bool SendCtrlC(Process process)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (process.HasExited)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!AttachConsole((uint)process.Id))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SetConsoleCtrlHandler(null, true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetConsoleCtrlHandler(null, false);
|
||||||
|
FreeConsole();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SendSignal(int pid, int signal)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
var killProcess = Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "kill",
|
||||||
|
Arguments = $"-{signal} {pid}",
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
});
|
||||||
|
|
||||||
|
killProcess?.WaitForExit();
|
||||||
|
return killProcess?.ExitCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows Console API imports
|
||||||
|
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||||||
|
public static extern bool AttachConsole(uint dwProcessId);
|
||||||
|
|
||||||
|
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||||||
|
public static extern bool FreeConsole();
|
||||||
|
|
||||||
|
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||||||
|
public static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandler? HandlerRoutine, bool Add);
|
||||||
|
|
||||||
|
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||||||
|
public static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);
|
||||||
|
|
||||||
|
public enum CtrlTypes
|
||||||
|
{
|
||||||
|
CTRL_C_EVENT = 0,
|
||||||
|
CTRL_BREAK_EVENT = 1,
|
||||||
|
CTRL_CLOSE_EVENT = 2,
|
||||||
|
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 }, ... ]
|
||||||
|
}
|
||||||
|
""")]
|
||||||
|
public static ListSessionsResult ListSessions()
|
||||||
|
{
|
||||||
|
var 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 }
|
||||||
|
}
|
||||||
|
""")]
|
||||||
|
public static CreateSessionResult CreateSession(
|
||||||
|
[Description("Memorable label for the new session.")] string label
|
||||||
|
)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
""")]
|
||||||
|
public static ExitSessionResult ExitSession(
|
||||||
|
[Description("ID of the session to exit.")] string sessionId
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (_sessions.TryRemove(sessionId, out var session))
|
||||||
|
{
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
""")]
|
||||||
|
public static SendSignalResult SendSignal(
|
||||||
|
[Description("Session ID to send the signal to.")] string sessionId,
|
||||||
|
[Description("Signal number to send (e.g., 2 for SIGINT)." )] int signal
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (_sessions.TryGetValue(sessionId, out var session))
|
||||||
|
{
|
||||||
|
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.
|
||||||
|
""")]
|
||||||
|
public static SendInputWithExceptResult SendInputWithExcept(
|
||||||
|
[Description("Session ID to send input to.")] string sessionId,
|
||||||
|
[Description("Input text to send (without newline)." )] string input,
|
||||||
|
[Description("Timeout in milliseconds.")] int timeout,
|
||||||
|
[Description("Regex patterns to match against output.")] params string[] excepts
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
matchedPattern = excepts[i];
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matched)
|
||||||
|
{
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch.Stop();
|
||||||
|
|
||||||
|
var (finalStdout, finalStderr) = session.GetAndClearBuffers();
|
||||||
|
|
||||||
|
return new SendInputWithExceptResult(
|
||||||
|
matched ? "except" : "timeout",
|
||||||
|
matchedPattern,
|
||||||
|
finalStdout,
|
||||||
|
finalStderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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'
|
||||||
|
}
|
||||||
|
""")]
|
||||||
|
public static SendInputAndForgetResult SendInputAndForget(
|
||||||
|
[Description("Session ID to send input to.")] string sessionId,
|
||||||
|
[Description("Input text to send (without newline)." )] string input
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (_sessions.TryGetValue(sessionId, out var session))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
session.SendInput(input);
|
||||||
|
return new SendInputAndForgetResult("success");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new SendInputAndForgetResult("fail");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
""")]
|
||||||
|
public static OneOffCommandResult OneOffCommand(
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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?$", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OneOffCommandResult(
|
||||||
|
completed ? "finish" : "timeout",
|
||||||
|
finalStdout,
|
||||||
|
finalStderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description("""
|
||||||
|
Get and clear cached stdout/stderr of the specified session.
|
||||||
|
Return Format:
|
||||||
|
{
|
||||||
|
stdout: string,
|
||||||
|
stderr: string
|
||||||
|
}
|
||||||
|
""")]
|
||||||
|
public static GetCachedResultResult GetCachedResult(
|
||||||
|
[Description("Session ID to retrieve output from.")] string sessionId
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (_sessions.TryGetValue(sessionId, out var session))
|
||||||
|
{
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
""")]
|
||||||
|
public static ClearResult Clear(
|
||||||
|
[Description("Session ID to clear cache for.")] string sessionId
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (_sessions.TryGetValue(sessionId, out var session))
|
||||||
|
{
|
||||||
|
session.ClearBuffers();
|
||||||
|
return new ClearResult("success");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClearResult("fail");
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Tools/YoutrackTool.cs
Normal file
8
Tools/YoutrackTool.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
namespace Alchegos.MCP.Tools;
|
||||||
|
[McpServerToolType]
|
||||||
|
public static class YoutrackTool
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user