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; public class GiteaApiLoggingHandler : DelegatingHandler { public GiteaApiLoggingHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) { Console.WriteLine("GiteaApiLoggingHandler constructor called"); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { 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); } } }