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 GitLabVersionTools( GitLabClientProvider provider, IConfiguration configuration, ToolRegistryService registry) { private readonly string _token = configuration["GitLab:Token"] ?? string.Empty; private readonly string _baseUrl = configuration["GitLab:Url"] ?? 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 GetVisibility(string visibility) => visibility switch { "public" => "Public", "internal" => "Internal", "private" => "Private", _ => visibility ?? "unknown" }; 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(); } /// /// Получить список тегов /// /// ID проекта /// Token отмены [McpServerTool, Description("Получить список тегов проекта")] public async Task ListVersions( [Description("ID проекта")] int projectId, CancellationToken cancellationToken = default) { if (!TryCheckEnabled("ListVersions", 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/tags", RestSharp.Method.Get); request.AddHeader("Accept", "application/json"); request.AddHeader("PRIVATE-TOKEN", _token); request.AddQueryParameter("per_page", "30"); var response = await client.ExecuteAsync(request, cancellationToken); if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content)) { return FormatResponseError("list_versions", response, $"/projects/{projectId}/repository/tags"); } using var document = JsonDocument.Parse(response.Content); var root = document.RootElement; if (!root.TryGetProperty("tags", out var tagsElement) || tagsElement.GetArrayLength() == 0) { return $"Тегов в проекте #{projectId} не найдено."; } var lines = new List(); foreach (var tag in tagsElement.EnumerateArray()) { var name = GetNestedString(tag, "name") ?? "-"; var commitSha = GetNestedString(tag, "commit", "sha") ?? "-"; var commitMessage = GetNestedString(tag, "commit", "message") ?? "-"; var tagType = GetNestedString(tag, "tag_type") ?? "unknown"; lines.Add($"'{name}' (type={tagType}, sha={commitSha})\n message: {commitMessage}"); } return $"Тег версии проекта #{projectId} ({tagsElement.GetArrayLength()} шт.):\n{string.Join('\n', lines)}"; } catch (Exception ex) { return FormatException("list_versions", ex); } } /// /// Создать тег /// /// ID проекта /// Имя тега /// Описание (опционально) /// Token отмены [McpServerTool, Description("Создать тег версии")] public async Task CreateVersion( [Description("ID проекта")] int projectId, [Description("Имя тега")] string name, [Description("Описание тега")] string? description = null, CancellationToken cancellationToken = default) { if (!TryCheckEnabled("CreateVersion", out var enabledError)) return enabledError; if (projectId <= 0) { return "ID проекта GitLab некорректно задан."; } if (string.IsNullOrWhiteSpace(name)) { return "Имя тега GitLab не может быть пустым."; } if (!TryGetClient(out var client, out var error)) return error; try { var request = new RestRequest($"/projects/{projectId}/repository/tags", RestSharp.Method.Post); request.AddHeader("Accept", "application/json"); request.AddHeader("PRIVATE-TOKEN", _token); request.AddHeader("Content-Type", "application/json"); var jsonBody = new { name = name, description = description ?? string.Empty }.ToJson(); request.AddJsonBody(jsonBody); var response = await client.ExecuteAsync(request, cancellationToken); if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content)) { return FormatResponseError("create_version", response, $"/projects/{projectId}/repository/tags"); } using var document = JsonDocument.Parse(response.Content); var root = document.RootElement; var tagName = GetNestedString(root, "name") ?? "-"; var sha = GetNestedString(root, "commit", "sha") ?? "-"; var refType = GetNestedString(root, "ref_type") ?? "-"; return $"Тег версии создан в проекте #{projectId}:\n'{tagName}' (ref_type={refType}, sha={sha})"; } catch (Exception ex) { return FormatException("create_version", ex); } } /// /// Удалить тег /// /// ID проекта /// Имя тега /// Token отмены [McpServerTool, Description("Удалить тег версии")] public async Task DeleteVersion( [Description("ID проекта")] int projectId, [Description("Имя тега")] string tagName, CancellationToken cancellationToken = default) { if (!TryCheckEnabled("DeleteVersion", out var enabledError)) return enabledError; if (projectId <= 0) { return "ID проекта GitLab некорректно задан."; } if (string.IsNullOrWhiteSpace(tagName)) { return "Имя тега GitLab не может быть пустым."; } if (!TryGetClient(out var client, out var error)) return error; try { var request = new RestRequest($"/projects/{projectId}/repository/tags/{tagName}", RestSharp.Method.Delete); 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("delete_version", response, $"/projects/{projectId}/repository/tags/{tagName}"); } using var document = JsonDocument.Parse(response.Content); var root = document.RootElement; var deletedTag = GetNestedString(root, "name") ?? tagName; return $"Тег '{deletedTag}' успешно удалён из проекта #{projectId}."; } catch (Exception ex) { return FormatException("delete_version", ex); } } } internal static class JsonExtensions { public static string ToJson(this object obj) => System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions { WriteIndented = false }); }