diff --git a/Alchegos.MCP.csproj b/Alchegos.MCP.csproj index f1d6f49..db20788 100644 --- a/Alchegos.MCP.csproj +++ b/Alchegos.MCP.csproj @@ -9,7 +9,7 @@ - + diff --git a/Dockerfile b/Dockerfile index f3b76dd..30c04c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/Filters/McpAuthFilter.cs b/Filters/McpAuthFilter.cs new file mode 100644 index 0000000..e287be7 --- /dev/null +++ b/Filters/McpAuthFilter.cs @@ -0,0 +1,53 @@ +using System.Net; +using Microsoft.Extensions.Primitives; + +namespace Alchegos.MCP.Filters; + +public class McpAuthFilter : IEndpointFilter +{ + private readonly ILogger _logger; + + public McpAuthFilter(ILogger logger) + { + _logger = logger; + } + + public async ValueTask 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; + } +} \ No newline at end of file diff --git a/Middleware/RequestBodyLoggingMiddleware.cs b/Middleware/RequestBodyLoggingMiddleware.cs deleted file mode 100644 index af6c89e..0000000 --- a/Middleware/RequestBodyLoggingMiddleware.cs +++ /dev/null @@ -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 logger) - { - _next = next; - _logger = logger; - } - - public async Task InvokeAsync(HttpContext context) - { - if (context.Request.Method == HttpMethods.Post && context.Request.ContentLength > 0) - { - context.Request.EnableBuffering(); - - using var reader = new StreamReader( - context.Request.Body, - encoding: Encoding.UTF8, - detectEncodingFromByteOrderMarks: false, - bufferSize: 4096, - leaveOpen: true); - - var requestBody = await reader.ReadToEndAsync(); - context.Request.Body.Position = 0; - - _logger.LogTrace("Request Body (POST {RequestPath}):\n{RequestBody}", context.Request.Path, requestBody); - - } - - await _next(context); - } - } -} - -namespace Microsoft.AspNetCore.Builder -{ - public static class RequestBodyLoggingMiddlewareExtensions - { - public static IApplicationBuilder UseRequestBodyLogging( - this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } -} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 356ef9b..968c5a5 100644 --- a/Program.cs +++ b/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(); diff --git a/Tools/GiteaTool.cs b/Tools/GiteaTool.cs index 526033d..9f49a3e 100644 --- a/Tools/GiteaTool.cs +++ b/Tools/GiteaTool.cs @@ -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 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 _apiKeyMap = new Dictionary + { + { "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 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 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 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 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 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 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); + } } } \ No newline at end of file diff --git a/appsettings.json b/appsettings.json index 909070d..ec04bc1 100644 --- a/appsettings.json +++ b/appsettings.json @@ -2,8 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Trace" - + "Microsoft.AspNetCore": "Warning" } - } + }, + "AllowedHosts": "*" } \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 120598e..0000000 --- a/entrypoint.sh +++ /dev/null @@ -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 diff --git a/playground.sh b/playground.sh deleted file mode 100644 index 677db01..0000000 --- a/playground.sh +++ /dev/null @@ -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 < [options] - -Commands: - list List all hosts configured for SSH connection - add [pub_key] - Add a new host; installs public key and saves config - delete Remove a host; deletes key from remote and config - edit [new_pub_key] - Replace the key for a given host - query Show the configuration for a given host - help Show this help message -EOF - exit 1 -} - -# Load entry by alias -load_entry() { - grep "^$1|" "$CONFIG_FILE" -} - -# List hosts -list_hosts() { - if [ ! -s "$CONFIG_FILE" ]; then - echo "No hosts configured." - exit 0 - fi - printf "%-15s %-25s %s\n" "ALIAS" "USER@HOST" "PRIVATE_KEY" - echo "---------------------------------------------------------------" - while IFS='|' read -r alias userhost privkey pubkey; do - printf "%-15s %-25s %s\n" "$alias" "$userhost" "$privkey" - done < "$CONFIG_FILE" -} - -# Add host -add_host() { - [ $# -lt 3 ] && echo "add requires alias, user@host, private key" && usage - alias="$1"; userhost="$2"; privkey="$3"; pubkey="$4" - [ -z "$pubkey" ] && pubkey="${privkey}.pub" - - if load_entry "$alias" >/dev/null; then - echo "Alias '$alias' already exists." && exit 1 - fi - - # Copy public key to remote - echo "Installing public key to $userhost..." - ssh-copy-id -i "$pubkey" "$userhost" || { echo "Failed to install key."; exit 1; } - - # Save to config - echo "$alias|$userhost|$privkey|$pubkey" >> "$CONFIG_FILE" - echo "Host '$alias' added." -} - -# Delete host -delete_host() { - [ $# -lt 1 ] && echo "delete requires alias" && usage - alias="$1" - entry=$(load_entry "$alias") - [ -z "$entry" ] && echo "Alias '$alias' not found." && exit 1 - - IFS='|' read -r _ userhost privkey pubkey < ~/.ssh/authorized_keys.tmp && \ - mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys" || echo "Warning: could not remove key on remote." - - # Remove from config - grep -v "^$alias|" "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" - echo "Host '$alias' deleted." -} - -# Edit host key -edit_host() { - [ $# -lt 2 ] && echo "edit requires alias and new private key" && usage - alias="$1"; newpriv="$2"; newpub="$3" - [ -z "$newpub" ] && newpub="${newpriv}.pub" - entry=$(load_entry "$alias") - [ -z "$entry" ] && echo "Alias '$alias' not found." && exit 1 - - IFS='|' read -r _ userhost oldpriv oldpub < ~/.ssh/authorized_keys.tmp && mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys" || echo "Warning: could not remove old key." - - # Install new key - echo "Installing new public key to $userhost..." - ssh-copy-id -i "$newpub" "$userhost" || { echo "Failed to install new key."; exit 1; } - - # Update config - grep -v "^$alias|" "$CONFIG_FILE" > "$CONFIG_FILE.tmp" - echo "$alias|$userhost|$newpriv|$newpub" >> "$CONFIG_FILE.tmp" - mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" - echo "Host '$alias' updated." -} - -# Query host -query_host() { - [ $# -lt 1 ] && echo "query requires alias" && usage - alias="$1" - entry=$(load_entry "$alias") - [ -z "$entry" ] && echo "Alias '$alias' not found." && exit 1 - IFS='|' read -r _ userhost privkey pubkey <