add: gitea tools
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Alchegos.Core" Version="0.0.1" />
|
||||
<PackageReference Include="Alchegos.Core" Version="0.0.4" />
|
||||
<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" />
|
||||
|
||||
@@ -27,7 +27,6 @@ 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"]
|
||||
RUN ssh-keygen -t rsa -b 4096 -N "" -q -f /root/.ssh/id_rsa
|
||||
COPY ./jetbrains_debugger_agent_20240726.40.0 /app/rdb
|
||||
ENTRYPOINT ["dotnet", "Alchegos.MCP.dll"]
|
||||
|
||||
53
Filters/McpAuthFilter.cs
Normal file
53
Filters/McpAuthFilter.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Alchegos.MCP.Filters;
|
||||
|
||||
public class McpAuthFilter : IEndpointFilter
|
||||
{
|
||||
private readonly ILogger<McpAuthFilter> _logger;
|
||||
|
||||
public McpAuthFilter(ILogger<McpAuthFilter> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
|
||||
{
|
||||
var expectedToken = Environment.GetEnvironmentVariable("MCP_API_KEY");
|
||||
|
||||
if (string.IsNullOrEmpty(expectedToken))
|
||||
{
|
||||
_logger.LogCritical("Required environment variable 'MCP_API_KEY' is not set.");
|
||||
return Results.StatusCode((int)HttpStatusCode.InternalServerError);
|
||||
}
|
||||
|
||||
var httpContext = context.HttpContext;
|
||||
|
||||
if (!httpContext.Request.Headers.TryGetValue("X-Api-Key", out StringValues receivedTokenValues) || receivedTokenValues.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("MCP request rejected: Missing 'X-Api-Key' header.");
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var receivedToken = receivedTokenValues.ToString();
|
||||
if (!SecureCompare(receivedToken, expectedToken))
|
||||
{
|
||||
_logger.LogWarning("MCP request rejected: Invalid API Key provided.");
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
_logger.LogInformation("MCP request authenticated successfully.");
|
||||
return await next(context);
|
||||
}
|
||||
|
||||
private static bool SecureCompare(string a, string b)
|
||||
{
|
||||
a ??= string.Empty;
|
||||
b ??= string.Empty;
|
||||
if (a.Length != b.Length) return false;
|
||||
int diff = 0;
|
||||
for (int i = 0; i < a.Length; i++) diff |= a[i] ^ b[i];
|
||||
return diff == 0;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Program.cs
36
Program.cs
@@ -1,3 +1,5 @@
|
||||
using Alchegos.MCP.Middleware;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
const int BIND_PORT = 5050;
|
||||
@@ -6,13 +8,41 @@ builder.Services
|
||||
.AddMcpServer()
|
||||
.WithHttpTransport()
|
||||
.WithToolsFromAssembly();
|
||||
|
||||
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.Use(async (context, next) =>
|
||||
{
|
||||
var expectedToken = Environment.GetEnvironmentVariable("MCP_API_KEY");
|
||||
if (string.IsNullOrEmpty(expectedToken))
|
||||
{
|
||||
context.Response.StatusCode = 500;
|
||||
await context.Response.WriteAsync("API Key not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.Request.Headers.TryGetValue("X-Api-Key", out var apiKey) ||
|
||||
string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("Missing API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiKey != expectedToken)
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("Invalid API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
app.MapMcp();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -1,18 +1,648 @@
|
||||
using ModelContextProtocol.Server;
|
||||
using System.ComponentModel;
|
||||
using Alchegos.Core.Services.Gitea;
|
||||
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using System;
|
||||
|
||||
namespace Alchegos.MCP.Tools;
|
||||
[McpServerToolType]
|
||||
public static class GiteaTool
|
||||
|
||||
public class GiteaApiLoggingHandler : DelegatingHandler
|
||||
{
|
||||
public static string Login(string a)
|
||||
public GiteaApiLoggingHandler(HttpMessageHandler innerHandler = null)
|
||||
: base(innerHandler ?? new HttpClientHandler())
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
Console.WriteLine("GiteaApiLoggingHandler constructor called");
|
||||
}
|
||||
|
||||
public static string GetGiteaRepoList(string apiKey)
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
try
|
||||
{
|
||||
Console.WriteLine("GiteaApiLoggingHandler.SendAsync called");
|
||||
Console.WriteLine($"Gitea API Request: {request.Method} {request.RequestUri}");
|
||||
Console.WriteLine($"Base Address: {request.RequestUri?.IsAbsoluteUri}");
|
||||
Console.WriteLine($"Request Headers: {string.Join(", ", request.Headers.Select(h => $"{h.Key}: {string.Join(", ", h.Value)}"))}");
|
||||
|
||||
if (request.Content != null)
|
||||
{
|
||||
var content = await request.Content.ReadAsStringAsync();
|
||||
Console.WriteLine($"Request Body: {content}");
|
||||
}
|
||||
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
Console.WriteLine($"Response Status: {response.StatusCode}");
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine($"Response Body: {responseContent}");
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in GiteaApiLoggingHandler: {ex}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerToolType, Description("Tools for interacting with Gitea API, providing repository management functionalities")]
|
||||
public static class GiteaTool
|
||||
{
|
||||
|
||||
private static readonly Dictionary<string, string> _apiKeyMap = new Dictionary<string, string>
|
||||
{
|
||||
{ "project-manager", Environment.GetEnvironmentVariable("PM_GITEA_ACCESS_TOKEN") },
|
||||
{ "developer", Environment.GetEnvironmentVariable("DEV_GITEA_ACCESS_TOKEN") },
|
||||
{ "analyst", Environment.GetEnvironmentVariable("AN_GITEA_ACCESS_TOKEN") },
|
||||
{ "reviewer", Environment.GetEnvironmentVariable("RE_GITEA_ACCESS_TOKEN") },
|
||||
{ "tester", Environment.GetEnvironmentVariable("TE_GITEA_ACCESS_TOKEN") },
|
||||
};
|
||||
|
||||
private static GiteaApiClient CreateClient(string role)
|
||||
{
|
||||
if (string.IsNullOrEmpty(role))
|
||||
throw new ArgumentException("role cannot be empty", nameof(role));
|
||||
|
||||
if (!_apiKeyMap.TryGetValue(role, out var apiKey))
|
||||
throw new ArgumentException($"Invalid role: {role}", nameof(role));
|
||||
|
||||
var baseUrl = Environment.GetEnvironmentVariable("GITEA_BASE_URL") ??
|
||||
throw new InvalidOperationException("GITEA_BASE_URL environment variable is not set");
|
||||
|
||||
Console.WriteLine($"Creating Gitea client with base URL: {baseUrl}");
|
||||
|
||||
var client = new HttpClient(new GiteaApiLoggingHandler());
|
||||
|
||||
var options = new GiteaApiOptions
|
||||
{
|
||||
BaseUrl = baseUrl,
|
||||
Token = apiKey
|
||||
};
|
||||
return new GiteaApiClient(client, Microsoft.Extensions.Options.Options.Create(options));
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Create a new repository in Gitea")]
|
||||
public static Repository GiteaCreateRepository(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Name of the repository")] string name,
|
||||
[Description("Description of the repository")] string description = "",
|
||||
[Description("Whether the repository should be private")] bool isPrivate = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(name));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
var body = new CreateRepoOption
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Private = isPrivate,
|
||||
Auto_init = true
|
||||
};
|
||||
return client.CreateCurrentUserRepoAsync(body).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to create repository '{name}' : {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("List all repositories of a Gitea organization")]
|
||||
public static ICollection<Repository> GiteaListOrganizationRepositories(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Name of the organization")] string org,
|
||||
[Description("Page number, starting from 1")] int? page = null,
|
||||
[Description("Number of items per page")] int? limit = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(org))
|
||||
throw new ArgumentException("Organization name cannot be empty", nameof(org));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
return client.OrgListReposAsync(org, page, limit).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to list repositories for organization '{org}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("List all repositories of a Gitea user")]
|
||||
public static ICollection<Repository> GiteaListUserRepositories(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Username of the repository owner")] string username,
|
||||
[Description("Page number, starting from 1")] int? page = null,
|
||||
[Description("Number of items per page")] int? limit = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(username))
|
||||
throw new ArgumentException("Username cannot be empty", nameof(username));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
return client.UserListReposAsync(username, page, limit).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to list repositories for user '{username}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Get detailed information about a Gitea repository")]
|
||||
public static Repository GiteaGetRepository(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(name));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
return client.RepoGetAsync(owner, name).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to get repository '{name}' for owner '{owner}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Delete a Gitea repository")]
|
||||
public static void GiteaDeleteRepository(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(name));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
client.RepoDeleteAsync(owner, name).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to delete repository '{name}' for owner '{owner}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Create a new wiki page in a Gitea repository")]
|
||||
public static WikiPage GiteaCreateWikiPage(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Title of the wiki page")] string title,
|
||||
[Description("Content of the wiki page")] string content,
|
||||
[Description("Commit message for the wiki change")] string message = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
if (string.IsNullOrEmpty(title))
|
||||
throw new ArgumentException("Wiki page title cannot be empty", nameof(title));
|
||||
if (string.IsNullOrEmpty(content))
|
||||
throw new ArgumentException("Wiki page content cannot be empty", nameof(content));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
var body = new CreateWikiPageOptions
|
||||
{
|
||||
Title = title,
|
||||
Content_base64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(content)),
|
||||
Message = string.IsNullOrEmpty(message) ? $"Create wiki page: {title}" : message
|
||||
};
|
||||
|
||||
return client.RepoCreateWikiPageAsync(owner, repo, body).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to create wiki page '{title}' in repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Edit an existing wiki page in a Gitea repository")]
|
||||
public static WikiPage GiteaEditWikiPage(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Title of the wiki page")] string pageName,
|
||||
[Description("New content of the wiki page")] string content,
|
||||
[Description("New title for the wiki page (optional)")] string newTitle = null,
|
||||
[Description("Commit message for the wiki change")] string message = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
if (string.IsNullOrEmpty(pageName))
|
||||
throw new ArgumentException("Wiki page name cannot be empty", nameof(pageName));
|
||||
if (string.IsNullOrEmpty(content))
|
||||
throw new ArgumentException("Wiki page content cannot be empty", nameof(content));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
var body = new CreateWikiPageOptions
|
||||
{
|
||||
Title = newTitle ?? pageName,
|
||||
Content_base64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(content)),
|
||||
Message = string.IsNullOrEmpty(message) ? $"Update wiki page: {pageName}" : message
|
||||
};
|
||||
|
||||
return client.RepoEditWikiPageAsync(owner, repo, pageName, body).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to edit wiki page '{pageName}' in repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("List all wiki pages in a Gitea repository")]
|
||||
public static ICollection<WikiPageMetaData> GiteaListWikiPages(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Page number, starting from 1")] int? page = null,
|
||||
[Description("Number of items per page")] int? limit = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
return client.RepoGetWikiPagesAsync(owner, repo, page, limit).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to list wiki pages in repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Get a specific wiki page from a Gitea repository")]
|
||||
public static WikiPage GiteaGetWikiPage(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Title of the wiki page")] string pageName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
if (string.IsNullOrEmpty(pageName))
|
||||
throw new ArgumentException("Wiki page name cannot be empty", nameof(pageName));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
return client.RepoGetWikiPageAsync(owner, repo, pageName).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to get wiki page '{pageName}' from repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Get the revision history of a wiki page")]
|
||||
public static WikiCommitList GiteaGetWikiPageHistory(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Title of the wiki page")] string pageName,
|
||||
[Description("Page number, starting from 1")] int? page = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
if (string.IsNullOrEmpty(pageName))
|
||||
throw new ArgumentException("Wiki page name cannot be empty", nameof(pageName));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
return client.RepoGetWikiPageRevisionsAsync(owner, repo, pageName, page).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to get revision history for wiki page '{pageName}' in repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Delete a wiki page from a Gitea repository")]
|
||||
public static void GiteaDeleteWikiPage(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Title of the wiki page")] string pageName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
if (string.IsNullOrEmpty(pageName))
|
||||
throw new ArgumentException("Wiki page name cannot be empty", nameof(pageName));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
client.RepoDeleteWikiPageAsync(owner, repo, pageName).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to delete wiki page '{pageName}' from repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Create a new branch in a Gitea repository")]
|
||||
public static Branch GiteaCreateBranch(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Name of the new branch")] string branchName,
|
||||
[Description("Name of the branch or commit to branch from")] string fromBranch)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
if (string.IsNullOrEmpty(branchName))
|
||||
throw new ArgumentException("Branch name cannot be empty", nameof(branchName));
|
||||
if (string.IsNullOrEmpty(fromBranch))
|
||||
throw new ArgumentException("Source branch cannot be empty", nameof(fromBranch));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
var body = new CreateBranchRepoOption
|
||||
{
|
||||
New_branch_name = branchName,
|
||||
Old_branch_name = fromBranch
|
||||
};
|
||||
|
||||
return client.RepoCreateBranchAsync(owner, repo, body).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to create branch '{branchName}' in repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Delete a branch from a Gitea repository")]
|
||||
public static void GiteaDeleteBranch(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Name of the branch to delete")] string branchName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
if (string.IsNullOrEmpty(branchName))
|
||||
throw new ArgumentException("Branch name cannot be empty", nameof(branchName));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
client.RepoDeleteBranchAsync(owner, repo, branchName).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to delete branch '{branchName}' from repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("List all branches in a Gitea repository")]
|
||||
public static ICollection<Branch> GiteaListBranches(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Page number, starting from 1")] int? page = null,
|
||||
[Description("Number of items per page")] int? limit = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
return client.RepoListBranchesAsync(owner, repo, page, limit).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to list branches in repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Get detailed information about a specific branch")]
|
||||
public static Branch GiteaGetBranch(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Name of the branch")] string branchName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
if (string.IsNullOrEmpty(branchName))
|
||||
throw new ArgumentException("Branch name cannot be empty", nameof(branchName));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
return client.RepoGetBranchAsync(owner, repo, branchName).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to get branch '{branchName}' from repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Create a new issue in a Gitea repository")]
|
||||
public static Issue GiteaCreateIssue(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Title of the issue")] string title,
|
||||
[Description("Body content of the issue")] string issueBody = "",
|
||||
[Description("Labels to apply to the issue (label IDs)")] ICollection<long> labels = null,
|
||||
[Description("Milestone to assign the issue to")] int? milestone = null,
|
||||
[Description("User to assign the issue to")] string assignee = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
if (string.IsNullOrEmpty(title))
|
||||
throw new ArgumentException("Issue title cannot be empty", nameof(title));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
var body = new CreateIssueOption
|
||||
{
|
||||
Title = title,
|
||||
Body = issueBody,
|
||||
Labels = labels,
|
||||
Milestone = milestone,
|
||||
Assignee = assignee
|
||||
};
|
||||
|
||||
return client.IssueCreateIssueAsync(owner, repo, body).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to create issue in repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Update an existing issue in a Gitea repository")]
|
||||
public static Issue GiteaEditIssue(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Index of the issue")] int index,
|
||||
[Description("New title of the issue")] string title = null,
|
||||
[Description("New body content of the issue")] string issueBody = null,
|
||||
[Description("New milestone to assign the issue to")] int? milestone = null,
|
||||
[Description("New user to assign the issue to")] string assignee = null,
|
||||
[Description("New state of the issue (open/closed)")] string state = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
var body = new EditIssueOption
|
||||
{
|
||||
Title = title,
|
||||
Body = issueBody,
|
||||
Milestone = milestone,
|
||||
Assignee = assignee,
|
||||
State = state
|
||||
};
|
||||
|
||||
return client.IssueEditIssueAsync(owner, repo, index, body).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to edit issue #{index} in repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Delete an issue from a Gitea repository")]
|
||||
public static void GiteaDeleteIssue(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Index of the issue")] int index)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
client.IssueDeleteAsync(owner, repo, index).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to delete issue #{index} from repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("List all issues in a Gitea repository")]
|
||||
public static ICollection<Issue> GiteaListIssues(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Filter issues by state (open/closed/all)")] string state = "open",
|
||||
[Description("Filter issues by labels (comma separated)")] string labels = null,
|
||||
[Description("Search query")] string q = null,
|
||||
[Description("Filter issues by type (issues/pulls)")] string type = null,
|
||||
[Description("Filter issues by milestones (comma separated)")] string milestones = null,
|
||||
[Description("Filter issues created by this user")] string created_by = null,
|
||||
[Description("Filter issues assigned by this user")] string assigned_by = null,
|
||||
[Description("Filter issues mentioning this user")] string mentioned_by = null,
|
||||
[Description("Page number, starting from 1")] int? page = null,
|
||||
[Description("Number of items per page")] int? limit = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
return client.IssueListIssuesAsync(
|
||||
owner,
|
||||
repo,
|
||||
state == "open" ? State3.Open : state == "closed" ? State3.Closed : State3.All,
|
||||
labels,
|
||||
q,
|
||||
type == "pulls" ? Type3.Pulls : Type3.Issues,
|
||||
milestones,
|
||||
null,
|
||||
null,
|
||||
created_by,
|
||||
assigned_by,
|
||||
mentioned_by,
|
||||
page,
|
||||
limit).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to list issues in repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Get detailed information about a specific issue")]
|
||||
public static Issue GiteaGetIssue(
|
||||
[Description("Your role")] string role,
|
||||
[Description("Organization or username that owns the repository")] string owner,
|
||||
[Description("Name of the repository")] string repo,
|
||||
[Description("Index of the issue")] int index)
|
||||
{
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
throw new ArgumentException("Owner cannot be empty", nameof(owner));
|
||||
if (string.IsNullOrEmpty(repo))
|
||||
throw new ArgumentException("Repository name cannot be empty", nameof(repo));
|
||||
|
||||
try
|
||||
{
|
||||
var client = CreateClient(role);
|
||||
return client.IssueGetIssueAsync(owner, repo, index).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to get issue #{index} from repository '{owner}/{repo}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Trace"
|
||||
|
||||
}
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/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
141
playground.sh
@@ -1,141 +0,0 @@
|
||||
#!/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