753 lines
30 KiB
C#
753 lines
30 KiB
C#
using System.Collections.Generic;
|
|
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 GitLabIssueTools(
|
|
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? GetNestedString(JsonElement element, params string[] path)
|
|
{
|
|
if (path.Length == 0) return null;
|
|
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();
|
|
}
|
|
|
|
private static int GetNestedInt(JsonElement element, params string[] path)
|
|
{
|
|
var raw = GetNestedString(element, path);
|
|
return int.TryParse(raw, out var value) ? value : 0;
|
|
}
|
|
|
|
private static string GetIid(int iid) => iid > 0 ? $"#{iid}" : "-";
|
|
|
|
/// <summary>
|
|
/// Получить список Issues
|
|
/// </summary>
|
|
/// <param name="projectId">ID проекта</param>
|
|
/// <param name="issueState">Состояние issue (опционально)</param>
|
|
/// <param name="cancellationToken">Token отмены</param>
|
|
[McpServerTool, Description("Получить список Issues проекта")]
|
|
public async Task<string> ListIssues(
|
|
[Description("ID проекта")] int projectId,
|
|
[Description("Состояние issue")] string? issueState = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!TryCheckEnabled("ListIssues", 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}/issues", RestSharp.Method.Get);
|
|
request.AddHeader("Accept", "application/json");
|
|
request.AddHeader("PRIVATE-TOKEN", _token);
|
|
request.AddQueryParameter("per_page", "30");
|
|
|
|
if (!string.IsNullOrWhiteSpace(issueState))
|
|
{
|
|
request.AddQueryParameter("state", issueState);
|
|
}
|
|
|
|
var response = await client.ExecuteAsync(request, cancellationToken);
|
|
|
|
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
|
{
|
|
return FormatResponseError("list_issues", response, $"/projects/{projectId}/issues");
|
|
}
|
|
|
|
using var document = JsonDocument.Parse(response.Content);
|
|
var root = document.RootElement;
|
|
|
|
if (!root.TryGetProperty("issues", out var issuesElement) || issuesElement.GetArrayLength() == 0)
|
|
{
|
|
return $"Issues в проекте #{projectId} не найдены.";
|
|
}
|
|
|
|
var lines = new List<string>();
|
|
foreach (var issue in issuesElement.EnumerateArray())
|
|
{
|
|
var iid = GetIid(GetNestedInt(issue, "iid"));
|
|
var title = GetNestedString(issue, "title") ?? "-";
|
|
var state = GetNestedString(issue, "state") ?? "-";
|
|
var created_at = GetNestedString(issue, "created_at") ?? "-";
|
|
var author = GetNestedString(issue, "author", "name") ?? "-";
|
|
var labels = GetNestedString(issue, "labels") ?? "-";
|
|
var labelsList = GetNestedString(issue, "labels") is string labelsStr && !string.IsNullOrWhiteSpace(labelsStr)
|
|
? labelsStr.Split(',').Select(l => $"[{l.Trim()}]").ToArray() ?? new[] { labelsStr }
|
|
: Array.Empty<string>();
|
|
|
|
lines.Add($"{iid} - {title} [{state}]\n author: {author}\n labels: {string.Join(", ", labelsList)}");
|
|
lines.Add($" created_at: {created_at}");
|
|
}
|
|
|
|
return $"Issues проекта #{projectId} ({issuesElement.GetArrayLength()} шт.):\n{string.Join(Environment.NewLine, lines)}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return FormatException("list_issues", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Получить список Issues без фильтрации по state
|
|
/// </summary>
|
|
/// <param name="projectId">ID проекта</param>
|
|
/// <param name="cancellationToken">Token отмены</param>
|
|
[McpServerTool, Description("Получить список Issues проекта (без фильтра state)")]
|
|
public async Task<string> ListIssuesSimple(
|
|
[Description("ID проекта")] int projectId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!TryCheckEnabled("ListIssuesSimple", 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}/issues", 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_issues_simple", response, $"/projects/{projectId}/issues");
|
|
}
|
|
|
|
using var document = JsonDocument.Parse(response.Content);
|
|
var root = document.RootElement;
|
|
|
|
if (!root.TryGetProperty("issues", out var issuesElement) || issuesElement.GetArrayLength() == 0)
|
|
{
|
|
return $"Issues в проекте #{projectId} не найдены.";
|
|
}
|
|
|
|
var lines = new List<string>();
|
|
foreach (var issue in issuesElement.EnumerateArray())
|
|
{
|
|
var iid = GetIid(GetNestedInt(issue, "iid"));
|
|
var title = GetNestedString(issue, "title") ?? "-";
|
|
var state = GetNestedString(issue, "state") ?? "-";
|
|
var author = GetNestedString(issue, "author", "name") ?? "-";
|
|
var description = GetNestedString(issue, "description") ?? "-";
|
|
|
|
lines.Add($"{iid} - {title} [{state}]\n author: {author}\n description: {description}");
|
|
}
|
|
|
|
return $"Issues проекта #{projectId} ({issuesElement.GetArrayLength()} шт.):\n{string.Join(Environment.NewLine, lines)}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return FormatException("list_issues_simple", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Получить конкретный Issue
|
|
/// </summary>
|
|
/// <param name="projectId">ID проекта</param>
|
|
/// <param name="issueIid">ID Issue</param>
|
|
/// <param name="cancellationToken">Token отмены</param>
|
|
[McpServerTool, Description("Получить конкретный Issue")]
|
|
public async Task<string> GetIssue(
|
|
[Description("ID проекта")] int projectId,
|
|
[Description("ID Issue")] int issueIid,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!TryCheckEnabled("GetIssue", out var enabledError)) return enabledError;
|
|
|
|
if (projectId <= 0)
|
|
{
|
|
return "ID проекта GitLab некорректно задан.";
|
|
}
|
|
|
|
if (issueIid <= 0)
|
|
{
|
|
return "ID Issue GitLab некорректно задан.";
|
|
}
|
|
|
|
if (!TryGetClient(out var client, out var error)) return error;
|
|
|
|
try
|
|
{
|
|
var request = new RestRequest($"/projects/{projectId}/issues/{issueIid}", RestSharp.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_issue", response, $"/projects/{projectId}/issues/{issueIid}");
|
|
}
|
|
|
|
using var document = JsonDocument.Parse(response.Content);
|
|
var root = document.RootElement;
|
|
|
|
var iid = GetIid(GetNestedInt(root, "iid"));
|
|
var title = GetNestedString(root, "title") ?? "-";
|
|
var state = GetNestedString(root, "state") ?? "-";
|
|
var author = GetNestedString(root, "author", "name") ?? "-";
|
|
var created_at = GetNestedString(root, "created_at") ?? "-";
|
|
var labels = GetNestedString(root, "labels") ?? "-";
|
|
var description = GetNestedString(root, "description") ?? "-";
|
|
var labelsList = GetNestedString(root, "labels") is string labelsStr && !string.IsNullOrWhiteSpace(labelsStr)
|
|
? labelsStr.Split(',').Select(l => $"[{l.Trim()}]").ToArray() ?? new[] { labelsStr }
|
|
: Array.Empty<string>();
|
|
|
|
return $"Issue #{iid} в проекте #{projectId}:\n{title} [{state}]\n author: {author}\n labels: {string.Join(", ", labelsList)}\n created_at: {created_at}\n description: {description}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return FormatException("get_issue", ex, $"/projects/{projectId}/issues/{issueIid}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Создать Issue
|
|
/// </summary>
|
|
/// <param name="projectId">ID проекта</param>
|
|
/// <param name="title">Заголовок Issue</param>
|
|
/// <param name="description">Описание (опционально)</param>
|
|
/// <param name="labels">Метки (опционально)</param>
|
|
/// <param name="assigneeId">ID назначаемого пользователя (опционально)</param>
|
|
/// <param name="cancellationToken">Token отмены</param>
|
|
[McpServerTool, Description("Создать Issue")]
|
|
public async Task<string> CreateIssue(
|
|
[Description("ID проекта")] int projectId,
|
|
[Description("Заголовок Issue")] string title,
|
|
[Description("Описание Issue")] string? description = null,
|
|
[Description("Метки Issue")] string? labels = null,
|
|
[Description("ID назначаемого пользователя")] int? assigneeId = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!TryCheckEnabled("CreateIssue", out var enabledError)) return enabledError;
|
|
|
|
if (projectId <= 0)
|
|
{
|
|
return "ID проекта GitLab некорректно задан.";
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(title))
|
|
{
|
|
return "Заголовок Issue GitLab не может быть пустым.";
|
|
}
|
|
|
|
if (!TryGetClient(out var client, out var error)) return error;
|
|
|
|
try
|
|
{
|
|
var request = new RestRequest($"/projects/{projectId}/issues", RestSharp.Method.Post);
|
|
request.AddHeader("Accept", "application/json");
|
|
request.AddHeader("PRIVATE-TOKEN", _token);
|
|
request.AddHeader("Content-Type", "application/json");
|
|
|
|
var payload = new Dictionary<string, object?>
|
|
{
|
|
["title"] = title,
|
|
["description"] = description ?? string.Empty
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(labels))
|
|
{
|
|
payload["labels"] = labels;
|
|
}
|
|
|
|
if (assigneeId.HasValue)
|
|
{
|
|
payload["assignee_id"] = assigneeId.Value;
|
|
}
|
|
|
|
request.AddJsonBody(payload);
|
|
|
|
var response = await client.ExecuteAsync(request, cancellationToken);
|
|
|
|
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
|
{
|
|
return FormatResponseError("create_issue", response, $"/projects/{projectId}/issues");
|
|
}
|
|
|
|
using var document = JsonDocument.Parse(response.Content);
|
|
var root = document.RootElement;
|
|
|
|
var iid = GetIid(GetNestedInt(root, "iid"));
|
|
var issueTitle = GetNestedString(root, "title") ?? "-";
|
|
var issueState = GetNestedString(root, "state") ?? "-";
|
|
|
|
return $"Issue успешно создан в проекте #{projectId}:\nID: {iid}\n{issueTitle} [{issueState}]";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return FormatException("create_issue", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Обновить Issue
|
|
/// </summary>
|
|
/// <param name="projectId">ID проекта</param>
|
|
/// <param name="issueIid">ID Issue</param>
|
|
/// <param name="subject">Новый заголовок (опционально)</param>
|
|
/// <param name="description">Новое описание (опционально)</param>
|
|
/// <param name="labels">Новые метки (опционально)</param>
|
|
/// <param name="state">Новое состояние (опционально)</param>
|
|
/// <param name="cancellationToken">Token отмены</param>
|
|
[McpServerTool, Description("Обновить Issue")]
|
|
public async Task<string> UpdateIssue(
|
|
[Description("ID проекта")] int projectId,
|
|
[Description("ID Issue")] int issueIid,
|
|
[Description("Новый заголовок")] string? subject = null,
|
|
[Description("Новое описание")] string? description = null,
|
|
[Description("Новые метки")] string? labels = null,
|
|
[Description("Новое состояние")] string? state = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!TryCheckEnabled("UpdateIssue", out var enabledError)) return enabledError;
|
|
|
|
if (projectId <= 0)
|
|
{
|
|
return "ID проекта GitLab некорректно задан.";
|
|
}
|
|
|
|
if (issueIid <= 0)
|
|
{
|
|
return "ID Issue GitLab некорректно задан.";
|
|
}
|
|
|
|
if (!TryGetClient(out var client, out var error)) return error;
|
|
|
|
try
|
|
{
|
|
var request = new RestRequest($"/projects/{projectId}/issues/{issueIid}", RestSharp.Method.Put);
|
|
request.AddHeader("Accept", "application/json");
|
|
request.AddHeader("PRIVATE-TOKEN", _token);
|
|
request.AddHeader("Content-Type", "application/json");
|
|
|
|
var payload = new Dictionary<string, object?>();
|
|
|
|
if (!string.IsNullOrWhiteSpace(subject))
|
|
{
|
|
payload["title"] = subject;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(description))
|
|
{
|
|
payload["description"] = description;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(labels))
|
|
{
|
|
payload["labels"] = labels;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(state))
|
|
{
|
|
payload["state_event"] = state;
|
|
}
|
|
|
|
request.AddJsonBody(payload);
|
|
|
|
var response = await client.ExecuteAsync(request, cancellationToken);
|
|
|
|
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
|
{
|
|
return FormatResponseError("update_issue", response, $"/projects/{projectId}/issues/{issueIid}");
|
|
}
|
|
|
|
using var document = JsonDocument.Parse(response.Content);
|
|
var root = document.RootElement;
|
|
|
|
var iid = GetIid(GetNestedInt(root, "iid"));
|
|
var issueTitle = GetNestedString(root, "title") ?? "-";
|
|
var issueState = GetNestedString(root, "state") ?? "-";
|
|
|
|
return $"Issue #{iid} ({issueTitle}) успешно обновлён в проекте #{projectId}.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return FormatException("update_issue", ex, $"/projects/{projectId}/issues/{issueIid}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Закрыть Issue
|
|
/// </summary>
|
|
/// <param name="projectId">ID проекта</param>
|
|
/// <param name="issueIid">ID Issue</param>
|
|
/// <param name="cancellationToken">Token отмены</param>
|
|
[McpServerTool, Description("Закрыть Issue")]
|
|
public async Task<string> CloseIssue(
|
|
[Description("ID проекта")] int projectId,
|
|
[Description("ID Issue")] int issueIid,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!TryCheckEnabled("CloseIssue", out var enabledError)) return enabledError;
|
|
|
|
if (projectId <= 0)
|
|
{
|
|
return "ID проекта GitLab некорректно задан.";
|
|
}
|
|
|
|
if (issueIid <= 0)
|
|
{
|
|
return "ID Issue GitLab некорректно задан.";
|
|
}
|
|
|
|
if (!TryGetClient(out var client, out var error)) return error;
|
|
|
|
try
|
|
{
|
|
var request = new RestRequest($"/projects/{projectId}/issues/{issueIid}", RestSharp.Method.Put);
|
|
request.AddHeader("Accept", "application/json");
|
|
request.AddHeader("PRIVATE-TOKEN", _token);
|
|
request.AddHeader("Content-Type", "application/json");
|
|
|
|
request.AddJsonBody(new { state_event = "close" });
|
|
|
|
var response = await client.ExecuteAsync(request, cancellationToken);
|
|
|
|
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
|
{
|
|
return FormatResponseError("close_issue", response, $"/projects/{projectId}/issues/{issueIid}");
|
|
}
|
|
|
|
using var document = JsonDocument.Parse(response.Content);
|
|
var root = document.RootElement;
|
|
|
|
var issueTitle = GetNestedString(root, "title") ?? "-";
|
|
|
|
return $"Issue #{issueIid} ({issueTitle}) успешно закрыт в проекте #{projectId}.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return FormatException("close_issue", ex, $"/projects/{projectId}/issues/{issueIid}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Открыть Issue
|
|
/// </summary>
|
|
/// <param name="projectId">ID проекта</param>
|
|
/// <param name="issueIid">ID Issue</param>
|
|
/// <param name="cancellationToken">Token отмены</param>
|
|
[McpServerTool, Description("Открыть Issue")]
|
|
public async Task<string> OpenIssue(
|
|
[Description("ID проекта")] int projectId,
|
|
[Description("ID Issue")] int issueIid,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!TryCheckEnabled("OpenIssue", out var enabledError)) return enabledError;
|
|
|
|
if (projectId <= 0)
|
|
{
|
|
return "ID проекта GitLab некорректно задан.";
|
|
}
|
|
|
|
if (issueIid <= 0)
|
|
{
|
|
return "ID Issue GitLab некорректно задан.";
|
|
}
|
|
|
|
if (!TryGetClient(out var client, out var error)) return error;
|
|
|
|
try
|
|
{
|
|
var request = new RestRequest($"/projects/{projectId}/issues/{issueIid}", RestSharp.Method.Put);
|
|
request.AddHeader("Accept", "application/json");
|
|
request.AddHeader("PRIVATE-TOKEN", _token);
|
|
request.AddHeader("Content-Type", "application/json");
|
|
|
|
request.AddJsonBody(new { state_event = "reopen" });
|
|
|
|
var response = await client.ExecuteAsync(request, cancellationToken);
|
|
|
|
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
|
{
|
|
return FormatResponseError("open_issue", response, $"/projects/{projectId}/issues/{issueIid}");
|
|
}
|
|
|
|
using var document = JsonDocument.Parse(response.Content);
|
|
var root = document.RootElement;
|
|
|
|
var issueTitle = GetNestedString(root, "title") ?? "-";
|
|
|
|
return $"Issue #{issueIid} ({issueTitle}) успешно открыт в проекте #{projectId}.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return FormatException("open_issue", ex, $"/projects/{projectId}/issues/{issueIid}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Получить замечания к Issue
|
|
/// </summary>
|
|
/// <param name="projectId">ID проекта</param>
|
|
/// <param name="issueIid">ID Issue</param>
|
|
/// <param name="cancellationToken">Token отмены</param>
|
|
[McpServerTool, Description("Получить замечания к Issue")]
|
|
public async Task<string> ListIssueNotes(
|
|
[Description("ID проекта")] int projectId,
|
|
[Description("ID Issue")] int issueIid,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!TryCheckEnabled("ListIssueNotes", out var enabledError)) return enabledError;
|
|
|
|
if (projectId <= 0)
|
|
{
|
|
return "ID проекта GitLab некорректно задан.";
|
|
}
|
|
|
|
if (issueIid <= 0)
|
|
{
|
|
return "ID Issue GitLab некорректно задан.";
|
|
}
|
|
|
|
if (!TryGetClient(out var client, out var error)) return error;
|
|
|
|
try
|
|
{
|
|
var request = new RestRequest($"/projects/{projectId}/issues/{issueIid}/notes", 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_issue_notes", response, $"/projects/{projectId}/issues/{issueIid}/notes");
|
|
}
|
|
|
|
using var document = JsonDocument.Parse(response.Content);
|
|
var root = document.RootElement;
|
|
|
|
if (!root.TryGetProperty("notes", out var notesElement) || notesElement.GetArrayLength() == 0)
|
|
{
|
|
return $"Замечаний к Issue #{issueIid} не найдено.";
|
|
}
|
|
|
|
var lines = new List<string>();
|
|
foreach (var note in notesElement.EnumerateArray())
|
|
{
|
|
var author = GetNestedString(note, "author", "name") ?? "-";
|
|
var createdAt = GetNestedString(note, "created_at") ?? "-";
|
|
var subject = GetNestedString(note, "subject") ?? "-";
|
|
var body = GetNestedString(note, "body") ?? "-";
|
|
|
|
lines.Add($"Author: {author}, Time: {createdAt}\n Subject: {subject}\n Body: {body}");
|
|
}
|
|
|
|
return $"Замечания к Issue #{issueIid} ({notesElement.GetArrayLength()} шт.):\n{string.Join(Environment.NewLine, lines)}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return FormatException("list_issue_notes", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Добавить замечание к Issue
|
|
/// </summary>
|
|
/// <param name="projectId">ID проекта</param>
|
|
/// <param name="issueIid">ID Issue</param>
|
|
/// <param name="body">Текст замечания</param>
|
|
/// <param name="subject">Заголовок замечания (опционально)</param>
|
|
/// <param name="cancellationToken">Token отмены</param>
|
|
[McpServerTool, Description("Добавить замечание к Issue")]
|
|
public async Task<string> CreateIssueNote(
|
|
[Description("ID проекта")] int projectId,
|
|
[Description("ID Issue")] int issueIid,
|
|
[Description("Текст замечания")] string body,
|
|
[Description("Заголовок замечания")] string? subject = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!TryCheckEnabled("CreateIssueNote", out var enabledError)) return enabledError;
|
|
|
|
if (projectId <= 0)
|
|
{
|
|
return "ID проекта GitLab некорректно задан.";
|
|
}
|
|
|
|
if (issueIid <= 0)
|
|
{
|
|
return "ID Issue GitLab некорректно задан.";
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(body))
|
|
{
|
|
return "Текст замечания GitLab не может быть пустым.";
|
|
}
|
|
|
|
if (!TryGetClient(out var client, out var error)) return error;
|
|
|
|
try
|
|
{
|
|
var request = new RestRequest($"/projects/{projectId}/issues/{issueIid}/notes", RestSharp.Method.Post);
|
|
request.AddHeader("Accept", "application/json");
|
|
request.AddHeader("PRIVATE-TOKEN", _token);
|
|
request.AddHeader("Content-Type", "application/json");
|
|
|
|
request.AddJsonBody(new
|
|
{
|
|
body,
|
|
subject = subject ?? string.Empty
|
|
});
|
|
|
|
var response = await client.ExecuteAsync(request, cancellationToken);
|
|
|
|
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
|
{
|
|
return FormatResponseError("create_issue_note", response, $"/projects/{projectId}/issues/{issueIid}/notes");
|
|
}
|
|
|
|
using var document = JsonDocument.Parse(response.Content);
|
|
var root = document.RootElement;
|
|
|
|
var noteId = GetNestedString(root, "id") ?? "-";
|
|
var noteSubject = GetNestedString(root, "subject") ?? "-";
|
|
var noteBody = GetNestedString(root, "body") ?? "-";
|
|
|
|
return $"Замечание успешно добавлено к Issue #{issueIid}:\nID: #{noteId}\n{noteSubject}\n{noteBody}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return FormatException("create_issue_note", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Удалить замечание из Issue
|
|
/// </summary>
|
|
/// <param name="projectId">ID проекта</param>
|
|
/// <param name="issueIid">ID Issue</param>
|
|
/// <param name="noteId">ID замечания</param>
|
|
/// <param name="cancellationToken">Token отмены</param>
|
|
[McpServerTool, Description("Удалить замечание из Issue")]
|
|
public async Task<string> DeleteIssueNote(
|
|
[Description("ID проекта")] int projectId,
|
|
[Description("ID Issue")] int issueIid,
|
|
[Description("ID замечания")] int noteId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!TryCheckEnabled("DeleteIssueNote", out var enabledError)) return enabledError;
|
|
|
|
if (projectId <= 0)
|
|
{
|
|
return "ID проекта GitLab некорректно задан.";
|
|
}
|
|
|
|
if (issueIid <= 0)
|
|
{
|
|
return "ID Issue GitLab некорректно задан.";
|
|
}
|
|
|
|
if (noteId <= 0)
|
|
{
|
|
return "ID замечания GitLab некорректно задан.";
|
|
}
|
|
|
|
if (!TryGetClient(out var client, out var error)) return error;
|
|
|
|
try
|
|
{
|
|
var request = new RestRequest($"/projects/{projectId}/issues/{issueIid}/notes/{noteId}", 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_issue_note", response, $"/projects/{projectId}/issues/{issueIid}/notes/{noteId}");
|
|
}
|
|
|
|
using var document = JsonDocument.Parse(response.Content);
|
|
var root = document.RootElement;
|
|
|
|
var noteSubject = GetNestedString(root, "subject") ?? "-";
|
|
|
|
return $"Замечание #{noteId} ({noteSubject}) успешно удалено из Issue #{issueIid}.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return FormatException("delete_issue_note", ex, $"/projects/{projectId}/issues/{issueIid}/notes/{noteId}");
|
|
}
|
|
}
|
|
} |