using System.ComponentModel; using System.Text.Json; using LazyBear.MCP.Services.ToolRegistry; using ModelContextProtocol.Server; using RestSharp; namespace LazyBear.MCP.Services.GitLab; public sealed class GitLabBranchTools( GitLabClientProvider provider, IConfiguration configuration, ToolRegistryService registry) { private readonly string _token = configuration["GitLab:Token"] ?? string.Empty; private const string ModuleName = "GitLab"; private bool TryCheckEnabled(string toolName, out string error) { if (!registry.IsToolEnabled(ModuleName, toolName)) { error = $"Инструмент '{toolName}' модуля GitLab отключён в TUI."; return false; } error = string.Empty; return true; } private bool TryGetClient(out RestSharp.IRestClient client, out string error) { if (provider.InitializationError is not null) { client = null!; error = $"GitLab клиент не инициализирован. Детали: {provider.InitializationError}"; return false; } var clientInstance = provider.GetClient(); if (clientInstance is null) { client = null!; error = "GitLab клиент не создан."; return false; } client = clientInstance.RestClient; error = string.Empty; return true; } private string FormatResponseError(string toolName, RestResponse response, string? resource = null) { var resourcePart = string.IsNullOrWhiteSpace(resource) ? string.Empty : $", resource='{resource}'"; var body = string.IsNullOrWhiteSpace(response.Content) ? "-" : response.Content; return $"Ошибка GitLab в tool '{toolName}'{resourcePart}: status={(int)response.StatusCode}, details={body}"; } private string FormatException(string toolName, Exception exception, string? resource = null) { var resourcePart = string.IsNullOrWhiteSpace(resource) ? string.Empty : $", resource='{resource}'"; return $"Ошибка GitLab в tool '{toolName}'{resourcePart}: {exception.GetType().Name}: {exception.Message}"; } private static string? GetNestedString(JsonElement element, params string[] path) { var current = element; foreach (var segment in path) { if (!current.TryGetProperty(segment, out current)) { return null; } } return current.ValueKind == JsonValueKind.String ? current.GetString() : current.ToString(); } [McpServerTool, Description("Получить список веток GitLab проекта")] public async Task ListBranches( [Description("ID проекта")] int projectId, CancellationToken cancellationToken = default) { if (!TryCheckEnabled("ListBranches", out var enabledError)) return enabledError; if (projectId <= 0) { return "ID проекта GitLab некорректно задан."; } if (!TryGetClient(out var client, out var error)) return error; try { var request = new RestRequest($"/projects/{projectId}/repository/branches", Method.Get); request.AddHeader("Accept", "application/json"); request.AddHeader("PRIVATE-TOKEN", _token); request.AddQueryParameter("per_page", "100"); var response = await client.ExecuteAsync(request, cancellationToken); if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content)) { return FormatResponseError("list_branches", response, $"/projects/{projectId}/repository/branches"); } using var document = JsonDocument.Parse(response.Content); var root = document.RootElement; if (root.ValueKind != JsonValueKind.Array || root.GetArrayLength() == 0) { return $"Ветки в проекте #{projectId} не найдены."; } var lines = new List(); foreach (var branch in root.EnumerateArray()) { var name = GetNestedString(branch, "name") ?? "-"; var isDefault = GetNestedString(branch, "default") ?? "false"; var isProtected = GetNestedString(branch, "protected") ?? "false"; var commit = GetNestedString(branch, "commit", "short_id") ?? GetNestedString(branch, "commit", "id") ?? "-"; lines.Add($"{name} (default={isDefault}, protected={isProtected}, commit={commit})"); } return $"Ветки проекта #{projectId} ({root.GetArrayLength()} шт.):{Environment.NewLine}{string.Join(Environment.NewLine, lines)}"; } catch (Exception ex) { return FormatException("list_branches", ex); } } [McpServerTool, Description("Получить ветку GitLab проекта")] public async Task GetBranch( [Description("ID проекта")] int projectId, [Description("Имя ветки")] string branchName, CancellationToken cancellationToken = default) { if (!TryCheckEnabled("GetBranch", out var enabledError)) return enabledError; if (projectId <= 0) { return "ID проекта GitLab некорректно задан."; } if (string.IsNullOrWhiteSpace(branchName)) { return "Имя ветки GitLab не может быть пустым."; } if (!TryGetClient(out var client, out var error)) return error; try { var encoded = Uri.EscapeDataString(branchName); var request = new RestRequest($"/projects/{projectId}/repository/branches/{encoded}", Method.Get); request.AddHeader("Accept", "application/json"); request.AddHeader("PRIVATE-TOKEN", _token); var response = await client.ExecuteAsync(request, cancellationToken); if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content)) { return FormatResponseError("get_branch", response, $"/projects/{projectId}/repository/branches/{encoded}"); } using var document = JsonDocument.Parse(response.Content); var root = document.RootElement; var name = GetNestedString(root, "name") ?? branchName; var isDefault = GetNestedString(root, "default") ?? "false"; var isProtected = GetNestedString(root, "protected") ?? "false"; var canPush = GetNestedString(root, "can_push") ?? "false"; var commitId = GetNestedString(root, "commit", "id") ?? "-"; return $"Ветка '{name}' проекта #{projectId}:{Environment.NewLine}default={isDefault}, protected={isProtected}, can_push={canPush}{Environment.NewLine}commit={commitId}"; } catch (Exception ex) { return FormatException("get_branch", ex); } } [McpServerTool, Description("Создать ветку GitLab проекта")] public async Task CreateBranch( [Description("ID проекта")] int projectId, [Description("Имя новой ветки")] string branchName, [Description("Ветка или SHA-реф источника")] string @ref, CancellationToken cancellationToken = default) { if (!TryCheckEnabled("CreateBranch", out var enabledError)) return enabledError; if (projectId <= 0) { return "ID проекта GitLab некорректно задан."; } if (string.IsNullOrWhiteSpace(branchName)) { return "Имя новой ветки GitLab не может быть пустым."; } if (string.IsNullOrWhiteSpace(@ref)) { return "Источник ветки (ref) GitLab не может быть пустым."; } if (!TryGetClient(out var client, out var error)) return error; try { var request = new RestRequest($"/projects/{projectId}/repository/branches", Method.Post); request.AddHeader("Accept", "application/json"); request.AddHeader("PRIVATE-TOKEN", _token); request.AddHeader("Content-Type", "application/json"); request.AddJsonBody(new { branch = branchName, @ref }); var response = await client.ExecuteAsync(request, cancellationToken); if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content)) { return FormatResponseError("create_branch", response, $"/projects/{projectId}/repository/branches"); } using var document = JsonDocument.Parse(response.Content); var root = document.RootElement; var name = GetNestedString(root, "name") ?? branchName; var commit = GetNestedString(root, "commit", "short_id") ?? GetNestedString(root, "commit", "id") ?? "-"; return $"Ветка '{name}' успешно создана в проекте #{projectId}. commit={commit}"; } catch (Exception ex) { return FormatException("create_branch", ex); } } [McpServerTool, Description("Удалить ветку GitLab проекта")] public async Task DeleteBranch( [Description("ID проекта")] int projectId, [Description("Имя ветки")] string branchName, CancellationToken cancellationToken = default) { if (!TryCheckEnabled("DeleteBranch", out var enabledError)) return enabledError; if (projectId <= 0) { return "ID проекта GitLab некорректно задан."; } if (string.IsNullOrWhiteSpace(branchName)) { return "Имя ветки GitLab не может быть пустым."; } if (!TryGetClient(out var client, out var error)) return error; try { var encoded = Uri.EscapeDataString(branchName); var request = new RestRequest($"/projects/{projectId}/repository/branches/{encoded}", Method.Delete); request.AddHeader("Accept", "application/json"); request.AddHeader("PRIVATE-TOKEN", _token); var response = await client.ExecuteAsync(request, cancellationToken); if (!response.IsSuccessful) { return FormatResponseError("delete_branch", response, $"/projects/{projectId}/repository/branches/{encoded}"); } return $"Ветка '{branchName}' успешно удалена из проекта #{projectId}."; } catch (Exception ex) { return FormatException("delete_branch", ex); } } [McpServerTool, Description("Защитить ветку GitLab проекта")] public async Task ProtectBranch( [Description("ID проекта")] int projectId, [Description("Имя ветки")] string branchName, CancellationToken cancellationToken = default) { if (!TryCheckEnabled("ProtectBranch", out var enabledError)) return enabledError; if (projectId <= 0) { return "ID проекта GitLab некорректно задан."; } if (string.IsNullOrWhiteSpace(branchName)) { return "Имя ветки GitLab не может быть пустым."; } if (!TryGetClient(out var client, out var error)) return error; try { var request = new RestRequest($"/projects/{projectId}/protected_branches", Method.Post); request.AddHeader("Accept", "application/json"); request.AddHeader("PRIVATE-TOKEN", _token); request.AddHeader("Content-Type", "application/json"); request.AddJsonBody(new { name = branchName }); var response = await client.ExecuteAsync(request, cancellationToken); if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content)) { return FormatResponseError("protect_branch", response, $"/projects/{projectId}/protected_branches"); } return $"Ветка '{branchName}' успешно защищена в проекте #{projectId}."; } catch (Exception ex) { return FormatException("protect_branch", ex); } } [McpServerTool, Description("Снять защиту с ветки GitLab проекта")] public async Task UnprotectBranch( [Description("ID проекта")] int projectId, [Description("Имя ветки")] string branchName, CancellationToken cancellationToken = default) { if (!TryCheckEnabled("UnprotectBranch", out var enabledError)) return enabledError; if (projectId <= 0) { return "ID проекта GitLab некорректно задан."; } if (string.IsNullOrWhiteSpace(branchName)) { return "Имя ветки GitLab не может быть пустым."; } if (!TryGetClient(out var client, out var error)) return error; try { var encoded = Uri.EscapeDataString(branchName); var request = new RestRequest($"/projects/{projectId}/protected_branches/{encoded}", Method.Delete); request.AddHeader("Accept", "application/json"); request.AddHeader("PRIVATE-TOKEN", _token); var response = await client.ExecuteAsync(request, cancellationToken); if (!response.IsSuccessful) { return FormatResponseError("unprotect_branch", response, $"/projects/{projectId}/protected_branches/{encoded}"); } return $"Защита с ветки '{branchName}' успешно снята в проекте #{projectId}."; } catch (Exception ex) { return FormatException("unprotect_branch", ex); } } }