Чистка
This commit is contained in:
@@ -1,154 +0,0 @@
|
|||||||
using System.Net.Http;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace LazyBear.Confluence;
|
|
||||||
|
|
||||||
public static class ConfluenceClientFactory
|
|
||||||
{
|
|
||||||
public static HttpClient CreateClient(string baseUrl, HttpClientHandler? handler = null)
|
|
||||||
{
|
|
||||||
handler ??= new HttpClientHandler();
|
|
||||||
|
|
||||||
return new HttpClient(handler)
|
|
||||||
{
|
|
||||||
BaseAddress = new Uri(baseUrl.TrimEnd('/')),
|
|
||||||
DefaultRequestHeaders =
|
|
||||||
{
|
|
||||||
HttpHeaders
|
|
||||||
{
|
|
||||||
["User-Agent"] = "LazyBear-Confluence-MCP/1.0",
|
|
||||||
["Accept"] = "application/json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ConfluenceHttpClientProvider CreateProvider(string baseUrl, string? token = null, HttpClientHandler? handler = null)
|
|
||||||
{
|
|
||||||
var httpHandler = handler ?? new HttpClientHandler();
|
|
||||||
var httpClient = CreateClient(baseUrl, httpHandler);
|
|
||||||
|
|
||||||
return new ConfluenceHttpClientProvider(httpClient, baseUrl, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ConfluenceHttpClientProvider
|
|
||||||
{
|
|
||||||
public HttpClient Client { get; }
|
|
||||||
public string BaseUrl => HttpBaseAddress?.Root ?? string.Empty;
|
|
||||||
public string? AccessToken { get; }
|
|
||||||
public string? InitializationError { get; }
|
|
||||||
|
|
||||||
private Uri? HttpBaseAddress;
|
|
||||||
|
|
||||||
public ConfluenceHttpClientProvider(HttpClient client, string baseUrl, string? token = null)
|
|
||||||
{
|
|
||||||
Client = client;
|
|
||||||
HttpBaseAddress = new Uri(baseUrl.TrimEnd('/'));
|
|
||||||
AccessToken = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConcurrentDictionary<string, JsonElement> Cache { get; } = new();
|
|
||||||
|
|
||||||
public async Task<T?> GetJsonAsync<T>(HttpRequestMessage request) where T : class
|
|
||||||
{
|
|
||||||
using var response = await Client.SendAsync(request).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
||||||
throw new HttpRequestException($"[{request.RequestMethod}] {request.RequestUri}: {response.StatusCode} {errorBody}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return await JsonSerializer.DeserializeAsync<T>(response.Content, request, JsonDefaults.DefaultSerializerOptions)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<JsonElement?> GetJsonAsync(HttpRequestMessage request)
|
|
||||||
{
|
|
||||||
if (Cache.TryGetValue(request.RequestUri!.ToString(), out var cached))
|
|
||||||
{
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var response = await Client.SendAsync(request).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
||||||
throw new HttpRequestException($"[{request.RequestMethod}] {request.RequestUri}: {response.StatusCode} {errorBody}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
||||||
var element = JsonDocument.Parse(json).RootElement;
|
|
||||||
|
|
||||||
Cache[request.RequestUri!.ToString()] = element;
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpRequestMessage CreateGetRequest(string resource)
|
|
||||||
{
|
|
||||||
return new HttpRequestMessage(HttpMethod.Get, HttpBaseAddress + resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpRequestMessage CreatePostRequest(string resource, object? body = null)
|
|
||||||
{
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, HttpBaseAddress + resource);
|
|
||||||
request.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json");
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpRequestMessage CreatePutRequest(string resource, object? body = null)
|
|
||||||
{
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Put, HttpBaseAddress + resource);
|
|
||||||
request.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json");
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpRequestMessage CreateDeleteRequest(string resource)
|
|
||||||
{
|
|
||||||
return new HttpRequestMessage(HttpMethod.Delete, HttpBaseAddress + resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetAccessToken(string token)
|
|
||||||
{
|
|
||||||
AccessToken = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetRequestHeaders(HttpHeaders headers)
|
|
||||||
{
|
|
||||||
var cloned = Client.DefaultRequestHeaders.Clone();
|
|
||||||
foreach (var header in headers)
|
|
||||||
{
|
|
||||||
cloned.Add(header.Key, header.Value);
|
|
||||||
}
|
|
||||||
Client.DefaultRequestHeaders = cloned;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddRequestHeader(string name, params string[] values)
|
|
||||||
{
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
Client.DefaultRequestHeaders.Add(name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class JsonDefaults
|
|
||||||
{
|
|
||||||
public static readonly JsonSerializerOptions DefaultSerializerOptions = new()
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
Converters = { new JsonStringEnumConverter() }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
using LazyBear.Confluence;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace LazyBear.Confluence;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public sealed class ConfluenceCloudTools(ConfluenceHttpClientProvider provider, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
private static readonly JsonSerializerOptions JsonDefaults = new()
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
Converters = { new JsonStringEnumConverter() }
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly ConfluenceHttpClientProvider _provider = provider;
|
|
||||||
private readonly string? _initializationError = provider.InitializationError;
|
|
||||||
private readonly string _baseUrl = _provider.HttpBaseAddress?.Root ?? string.Empty;
|
|
||||||
private readonly string _defaultSpace = configuration["Confluence:DefaultSpace"] ?? "";
|
|
||||||
|
|
||||||
private HttpRequestMessage CreateGetRequest(string resource)
|
|
||||||
{
|
|
||||||
return new HttpRequestMessage(HttpMethod.Get, _baseUrl + resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpRequestMessage CreatePostRequest(string resource, object? body = null)
|
|
||||||
{
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, _baseUrl + resource);
|
|
||||||
request.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json");
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpRequestMessage CreatePutRequest(string resource, object? body = null)
|
|
||||||
{
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Put, _baseUrl + resource);
|
|
||||||
request.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json");
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpRequestMessage CreateDeleteRequest(string resource)
|
|
||||||
{
|
|
||||||
return new HttpRequestMessage(HttpMethod.Delete, _baseUrl + resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
private (bool Success, string Message) TryGetRequest(
|
|
||||||
HttpRequestMessage request,
|
|
||||||
Func<Task<JsonElement?>> executor,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
true,
|
|
||||||
string.Empty
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список страниц Confluence")]
|
|
||||||
public async Task<string> ListPagesAsync(
|
|
||||||
[Description("Пространство (key)")] string? spaceKey = null,
|
|
||||||
[Description("ID родителя")] string? parentId = null,
|
|
||||||
[Description("Максимум результатов")] int limit = 20,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(spaceKey))
|
|
||||||
{
|
|
||||||
return $"Пространство Confluence не задено: {spaceKey}";
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = $"rest/api/cloud/content?type=page&space={spaceKey}&limit={limit}";
|
|
||||||
var request = CreateGetRequest(resource);
|
|
||||||
|
|
||||||
// В реальном коде будет реальная логика
|
|
||||||
return "Pages listed for space: " + spaceKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Получить Confluence страницу")]
|
|
||||||
public async Task<string> GetPageAsync(
|
|
||||||
[Description("ID страницы или ручка")] string pageId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId))
|
|
||||||
{
|
|
||||||
return "ID страницы Confluence не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = $"rest/api/cloud/content/{pageId}?expand=body.storage,representation,ancestors";
|
|
||||||
var request = CreateGetRequest(resource);
|
|
||||||
|
|
||||||
return "Page retrieved.";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Создать Confluence страницу")]
|
|
||||||
public async Task<string> CreatePageAsync(
|
|
||||||
[Description("Заголовок")] string title,
|
|
||||||
[Description("Пространство")] string spaceKey,
|
|
||||||
[Description("Контент")] string content,
|
|
||||||
[Description("ID родителя")] string? parentId = null,
|
|
||||||
[Description("Теги")] string[]? labels = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(title))
|
|
||||||
{
|
|
||||||
return "Заголовок страницы Confluence не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var pageObject = new ConfluencePage
|
|
||||||
{
|
|
||||||
Title = title,
|
|
||||||
SpaceKey = spaceKey
|
|
||||||
};
|
|
||||||
|
|
||||||
return "Page created.";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Обновить Confluence страницу")]
|
|
||||||
public async Task<string> UpdatePageAsync(
|
|
||||||
[Description("ID страницы")] string pageId,
|
|
||||||
[Description("Новый заголовок")] string? title = null,
|
|
||||||
[Description("Новый контент")] string? content = null,
|
|
||||||
[Description("Новые теги")] string[]? labels = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId))
|
|
||||||
{
|
|
||||||
return "ID страницы Confluence не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Page updated.";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Удалить Confluence страницу")]
|
|
||||||
public async Task<string> DeletePageAsync(
|
|
||||||
[Description("ID страницы")] string pageId,
|
|
||||||
[Description("Удалить перманентно?")] bool permanent = false,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId))
|
|
||||||
{
|
|
||||||
return "ID страницы Confluence не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Page deleted.";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Поиск страниц Confluence")]
|
|
||||||
public async Task<string> SearchPagesAsync(
|
|
||||||
[Description("Запрос")] string? q = null,
|
|
||||||
[Description("Пространство")] string? spaceKey = null,
|
|
||||||
[Description("Типы контента")] string[]? types = null,
|
|
||||||
[Description("Максимум результатов")] int limit = 20,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var qText = q ?? "title~"""" OR body~""""";
|
|
||||||
var request = CreateGetRequest("rest/api/cloud/search");
|
|
||||||
request.AddQueryParameter("cql", qText);
|
|
||||||
request.AddQueryParameter("limit", limit.ToString());
|
|
||||||
|
|
||||||
return "Search results.";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список тегов страницы Confluence")]
|
|
||||||
public async Task<string> GetPageLabelsAsync(
|
|
||||||
[Description("ID страницы")] string pageId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId))
|
|
||||||
{
|
|
||||||
return "ID страницы Confluence не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = $"rest/api/cloud/content/{pageId}/labels";
|
|
||||||
var request = CreateGetRequest(resource);
|
|
||||||
|
|
||||||
return "Labels retrieved.";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Добавить тег на страницу Confluence")]
|
|
||||||
public async Task<string> AddPageLabelAsync(
|
|
||||||
[Description("ID страницы")] string pageId,
|
|
||||||
[Description("Тег")] string label,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId) || string.IsNullOrWhiteSpace(label))
|
|
||||||
{
|
|
||||||
return "pageId или label не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestBody = new
|
|
||||||
{
|
|
||||||
label,
|
|
||||||
type = "label"
|
|
||||||
};
|
|
||||||
|
|
||||||
var request = CreatePostRequest($"rest/api/cloud/content/{pageId}/labels", requestBody);
|
|
||||||
|
|
||||||
return "Label added.";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Удалить тег со страницы Confluence")]
|
|
||||||
public async Task<string> RemovePageLabelAsync(
|
|
||||||
[Description("ID страницы")] string pageId,
|
|
||||||
[Description("Тег")] string label,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId) || string.IsNullOrWhiteSpace(label))
|
|
||||||
{
|
|
||||||
return "pageId или label не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = CreateDeleteRequest($"rest/api/cloud/content/{pageId}/labels/{label}");
|
|
||||||
|
|
||||||
return "Label removed.";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список всех тегов (глобальный)")]
|
|
||||||
public async Task<string> ListLabelsAsync(
|
|
||||||
[Description("Пространство")] string? spaceKey = null,
|
|
||||||
[Description("Максимум тегов")] int limit = 20,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var resource = spaceKey == null
|
|
||||||
? "rest/api/cloud/label/"
|
|
||||||
: $"rest/api/cloud/label/?spaceKeys={spaceKey}";
|
|
||||||
|
|
||||||
return "Labels listed.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ConfluencePage
|
|
||||||
{
|
|
||||||
public string Title { get; set; } = string.Empty;
|
|
||||||
public string SpaceKey { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using LazyBear.Confluence;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace LazyBear.Confluence;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public sealed class ConfluenceCommentsTools(ConfluenceHttpClientProvider provider)
|
|
||||||
{
|
|
||||||
private readonly ConfluenceHttpClientProvider _provider = provider;
|
|
||||||
private readonly string? _initializationError = provider.InitializationError;
|
|
||||||
|
|
||||||
private HttpRequestMessage CreateGetRequest(string resource)
|
|
||||||
{
|
|
||||||
return new HttpRequestMessage(HttpMethod.Get, resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpRequestMessage CreatePostRequest(string resource, object? body = null)
|
|
||||||
{
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, resource);
|
|
||||||
request.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json");
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpRequestMessage CreatePutRequest(string resource, object? body = null)
|
|
||||||
{
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Put, resource);
|
|
||||||
request.Content = new StringContent(JsonSerializer.Serialize(body), System.Text.Encoding.UTF8, "application/json");
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpRequestMessage CreateDeleteRequest(string resource)
|
|
||||||
{
|
|
||||||
return new HttpRequestMessage(HttpMethod.Delete, resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список комментариев Confluence страницы")]
|
|
||||||
public async Task<string> ListCommentsAsync(
|
|
||||||
[Description("ID страницы")] string pageId,
|
|
||||||
[Description("Параметры")] string? expand = null,
|
|
||||||
[Description("Максимум комментариев")] int? limit = 20,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId))
|
|
||||||
{
|
|
||||||
return "ID страницы Confluence не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = $"rest/api/cloud/content/{pageId}/comment";
|
|
||||||
var request = CreateGetRequest(resource);
|
|
||||||
|
|
||||||
return "Comments: [" + LimitToString(limit) + "]";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Добавить комментарий Confluence")]
|
|
||||||
public async Task<string> AddCommentAsync(
|
|
||||||
[Description("ID страницы")] string pageId,
|
|
||||||
[Description("Комментарий")] string body,
|
|
||||||
[Description("Имя пользователя")] string? avatar = null,
|
|
||||||
[Description("Тип комментария")] string? type = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId) || string.IsNullOrWhiteSpace(body))
|
|
||||||
{
|
|
||||||
return "pageId или body не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestBody = new
|
|
||||||
{
|
|
||||||
body = body,
|
|
||||||
avatarUrl = avatar,
|
|
||||||
type = type ?? "comment"
|
|
||||||
};
|
|
||||||
|
|
||||||
var request = CreatePostRequest($"rest/api/cloud/content/{pageId}/comment", requestBody);
|
|
||||||
|
|
||||||
return "Comment added.";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Удалить комментарий Confluence")]
|
|
||||||
public async Task<string> DeleteCommentAsync(
|
|
||||||
[Description("ID страницы")] string pageId,
|
|
||||||
[Description("ID комментария")] string commentId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId) || string.IsNullOrWhiteSpace(commentId))
|
|
||||||
{
|
|
||||||
return "pageId или commentId не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = CreateDeleteRequest($"rest/api/cloud/content/{pageId}/comment/{commentId}");
|
|
||||||
|
|
||||||
return "Comment deleted.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string LimitToString(int? limit)
|
|
||||||
{
|
|
||||||
return limit.HasValue && limit <= 0
|
|
||||||
? "0"
|
|
||||||
: limit.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
using LazyBear.Confluence;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace LazyBear.Confluence;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public sealed class ConfluenceDataCenterTools(ConfluenceHttpClientProvider provider, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
private static readonly JsonSerializerOptions JsonDefaults = new()
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
Converters = { new JsonStringEnumConverter() }
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly ConfluenceHttpClientProvider _confluence = provider;
|
|
||||||
private readonly string? _initializationError = provider.InitializationError;
|
|
||||||
private readonly string? _baseUrl = provider.HttpBaseAddress?.Root;
|
|
||||||
private readonly string _defaultSpace = configuration["Confluence:DefaultSpace"] ?? "default";
|
|
||||||
|
|
||||||
[McpServerTool, Description("Получить Confluence страницу по ID")]
|
|
||||||
public Task<string?> GetPageById(string pageId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId))
|
|
||||||
{
|
|
||||||
return Task.FromResult<string?>(("ID страницы не задан."));
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = $"rest/api/content/{pageId}";
|
|
||||||
var request = _confluence.CreateGetRequest(resource);
|
|
||||||
var headers = GetAuthHeaders();
|
|
||||||
|
|
||||||
request.Headers.CopyTo(headers);
|
|
||||||
headerRequest.Headers.Add("X-Atlassian-Token", "no-check");
|
|
||||||
|
|
||||||
Task<Page?> Execute()
|
|
||||||
{
|
|
||||||
var result = _confluence.GetJsonAsync<Page>(request);
|
|
||||||
return Task.FromResult(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(
|
|
||||||
Execute()
|
|
||||||
.ContinueWith(t =>
|
|
||||||
{
|
|
||||||
if (t.IsCompletedSuccessfully)
|
|
||||||
{
|
|
||||||
var page = t.Result ?? PageNotFound($"Page {pageId}");
|
|
||||||
return FormatPageResponse(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.Exception?.Message ?? "Внутренняя ошибка.";
|
|
||||||
}
|
|
||||||
).Result
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список страниц Confluence в пространстве")]
|
|
||||||
public Task<string?> ListPages(string? spaceKey = null, string? parentPageId = null)
|
|
||||||
{
|
|
||||||
spaceKey ??= spaceKey ?? ResolveSpaceKey();
|
|
||||||
|
|
||||||
string ResolveSpaceKey() => spaceKey ?? _defaultSpace;
|
|
||||||
|
|
||||||
var resource = "rest/api/content?type=page&limit=100";
|
|
||||||
var request = _confluence.CreateGetRequest(resource);
|
|
||||||
var headers = GetAuthHeaders();
|
|
||||||
|
|
||||||
request.Headers.CopyTo(headers);
|
|
||||||
headerRequest.Headers.Add("X-Atlassian-Token", "no-check");
|
|
||||||
|
|
||||||
string? spaceKey;
|
|
||||||
|
|
||||||
Task<List<Page>> Execute()
|
|
||||||
{
|
|
||||||
var result = _confluence.GetJsonAsync<Typo>(request);
|
|
||||||
return Task.FromResult(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(
|
|
||||||
Execute()
|
|
||||||
.ContinueWith(t =>
|
|
||||||
{
|
|
||||||
if (t.IsCompletedSuccessfully)
|
|
||||||
{
|
|
||||||
return t.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.Exception?.Message ?? "Внутренняя ошибка.";
|
|
||||||
}
|
|
||||||
).Result
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Создать Confluence страницу")]
|
|
||||||
public Task<string?> CreatePage(
|
|
||||||
string title,
|
|
||||||
string spaceKey,
|
|
||||||
string content,
|
|
||||||
string? parentId = null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(spaceKey))
|
|
||||||
{
|
|
||||||
return Task.FromResult<string?>(("title и spaceKey не заданы."));
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestBody = new
|
|
||||||
{
|
|
||||||
type = "page",
|
|
||||||
title,
|
|
||||||
properties = new
|
|
||||||
{
|
|
||||||
content = content,
|
|
||||||
restriction = new { }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var resource = spaceKey == ""
|
|
||||||
? $"rest/api/content?t=page\&limit=100\&parentId={parentId}"
|
|
||||||
: $"rest/api/content?type=page&title={title}";
|
|
||||||
|
|
||||||
return Task.FromResult<string?>((""));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string PageNotFound(string id) => $"Страница '{id}' не найдена.";
|
|
||||||
|
|
||||||
private static string FormatPageResponse(Page? page)
|
|
||||||
{
|
|
||||||
if (page == null)
|
|
||||||
{
|
|
||||||
return PageNotFound("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Title: {page.Title} {page.Id}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HttpRequestMessage headerRequest;
|
|
||||||
private static HttpRequestMessage headers;
|
|
||||||
|
|
||||||
private static HttpRequestMessage GetAuthHeaders()
|
|
||||||
{
|
|
||||||
var request = new HttpRequestMessage()
|
|
||||||
{
|
|
||||||
Headers =
|
|
||||||
{
|
|
||||||
[
|
|
||||||
"Accept",
|
|
||||||
"application/json"
|
|
||||||
] = null,
|
|
||||||
[
|
|
||||||
"Authorization",
|
|
||||||
"Bearer " + " "
|
|
||||||
] = null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ResolveSpaceKey() => _defaultSpace;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace LazyBear.Confluence;
|
|
||||||
|
|
||||||
public sealed class ConfluencePage
|
|
||||||
{
|
|
||||||
[JsonPropertyName("id")] public long Id { get; set; }
|
|
||||||
[JsonPropertyName("type")] public string Type { get; set; } = string.Empty;
|
|
||||||
[JsonPropertyName("title")] public string Title { get; set; } = string.Empty;
|
|
||||||
[JsonPropertyName("space")] public ConfluenceSpace? Space { get; set; }
|
|
||||||
[JsonPropertyName("version")] public ConfluenceVersion? Version { get; set; }
|
|
||||||
[JsonPropertyName("body")] public ConfluenceBody? Body { get; set; }
|
|
||||||
[JsonPropertyName("ancestors")] public List<ConfluencePage> Ancestors { get; set; } = new();
|
|
||||||
[JsonPropertyName("children")] public List<ConfluencePage> Children { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ConfluenceSpace
|
|
||||||
{
|
|
||||||
[JsonPropertyName("key")] public string Key { get; set; } = string.Empty;
|
|
||||||
[JsonPropertyName("name")] public string Name { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ConfluenceVersion
|
|
||||||
{
|
|
||||||
[JsonPropertyName("number")] public int Number { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ConfluenceBody
|
|
||||||
{
|
|
||||||
[JsonPropertyName("storage")] public BodyStorage? Storage { get; set; }
|
|
||||||
[JsonPropertyName("representation")] public string Representation { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class BodyStorage
|
|
||||||
{
|
|
||||||
[JsonPropertyName("value")] public string Value { get; set; } = string.Empty;
|
|
||||||
[JsonPropertyName("representation")] public string Representation { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
using LazyBear.Confluence;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace LazyBear.Confluence;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public sealed class ConfluenceSpacesTools(ConfluenceHttpClientProvider provider)
|
|
||||||
{
|
|
||||||
private readonly ConfluenceHttpClientProvider _provider = provider;
|
|
||||||
private readonly string? _initializationError = provider.InitializationError;
|
|
||||||
|
|
||||||
private HttpRequestMessage CreateGetRequest(string resource)
|
|
||||||
{
|
|
||||||
return new HttpRequestMessage(HttpMethod.Get, resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список всех пространств Confluence")]
|
|
||||||
public async Task<string> ListSpacesAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var resource = "rest/api/cloud/space";
|
|
||||||
var request = CreateGetRequest(resource);
|
|
||||||
|
|
||||||
return "Spaces: [" + LimitToSpaces(resource, request) + "]";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Получить детали пространства Confluence")]
|
|
||||||
public async Task<string> GetSpaceAsync(
|
|
||||||
[Description("key пространства")] string? key = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(key))
|
|
||||||
{
|
|
||||||
return "key пространства Confluence не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = $"rest/api/cloud/space/{key}";
|
|
||||||
var request = CreateGetRequest(resource);
|
|
||||||
|
|
||||||
return "Space details: " + key;
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Иерархия пространств Confluence")]
|
|
||||||
public async Task<string> GetSpacesHierarchyAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return "Hierarchy: [" + LimitToSpaces("", null) + "]";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string LimitToSpaces(string resource, HttpRequestMessage? request)
|
|
||||||
{
|
|
||||||
return "Spaces: [" + resource + "]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
using LazyBear.Confluence;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace LazyBear.Confluence.Pages;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public sealed class ConfluencePagesTools(ConfluenceHttpClientProvider provider)
|
|
||||||
{
|
|
||||||
private readonly ConfluenceHttpClientProvider _provider = provider;
|
|
||||||
|
|
||||||
[McpServerTool, Description("Список страниц Confluence")]
|
|
||||||
public async Task<string> ListPagesAsync(
|
|
||||||
[Description("Пространство")] string? spaceKey = null,
|
|
||||||
[Description("ID родитель")] string? parentId = null,
|
|
||||||
[Description("Максимум")] int? limit = 20,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(spaceKey))
|
|
||||||
{
|
|
||||||
return "spaceKey не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = $"{spaceKey}/pages";
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, resource);
|
|
||||||
|
|
||||||
return $"Список страниц в {spaceKey}";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Получить страницу Confluence")]
|
|
||||||
public async Task<string> GetPageAsync(
|
|
||||||
[Description("ID страницы")] string pageId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId))
|
|
||||||
{
|
|
||||||
return "pageId не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Страница: {pageId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Создать страницу")]
|
|
||||||
public async Task<string> CreatePageAsync(
|
|
||||||
[Description("Заголовок")] string title,
|
|
||||||
[Description("Контент")] string content,
|
|
||||||
[Description("Пространство")] string spaceKey,
|
|
||||||
[Description("ID родитель")] string? parentId = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(title))
|
|
||||||
{
|
|
||||||
return "title не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, $"rest/api/content?typeName=page");
|
|
||||||
|
|
||||||
return $"Страница создана: {title}";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Обновить страницу")]
|
|
||||||
public async Task<string> UpdatePageAsync(
|
|
||||||
[Description("ID")] string id,
|
|
||||||
[Description("Заголовок")] string? title = null,
|
|
||||||
[Description("Контент")] string? content = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
|
||||||
{
|
|
||||||
return "id не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Страница обновлена: {id}";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Удалить страницу")]
|
|
||||||
public async Task<string> DeletePageAsync(
|
|
||||||
[Description("ID страницы")] string pageId,
|
|
||||||
[Description("Перманентно?")] bool? permanent = false,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId))
|
|
||||||
{
|
|
||||||
return "pageId не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Страница удалена: {pageId}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using System.IO;
|
|
||||||
|
|
||||||
DirectoryInfo confluenceFolder = new DirectoryPathInfo("E:\Codex\LazyBearWorks\Libraries\Confluence");
|
|
||||||
|
|
||||||
if (confluenceFolder.Exists)
|
|
||||||
{
|
|
||||||
FileInfo[] files = confluenceFolder.GetFiles();
|
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Found file: " + file.FullName + " Size: " + file.Length + " bytes");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("Confluence folder not found!");
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.HttpResults;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using ModelContextProtocol.AspNetCore;
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
|
|
||||||
services.AddHttpClient().ConfigureHttpMessageHandlerBuilder(httpBuilder =>
|
|
||||||
{
|
|
||||||
httpBuilder.PrimaryHandler.MessageHandlerOptions.Events += (sender, e) =>
|
|
||||||
{
|
|
||||||
var request = sender;
|
|
||||||
|
|
||||||
if (!request.RequestUri.Equals(HttpResponseMessage.DefaultRequest?.RequestUri, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
var httpVersion = request.Version;
|
|
||||||
var headers = default(HttpHeaders?);
|
|
||||||
var uri = default(HttpRequestUri?);
|
|
||||||
var protocolVersion = default(HttpVersion?);
|
|
||||||
|
|
||||||
if (Uri.TryCreate(request.RequestUri, UriKind.Absolute, out uri))
|
|
||||||
{
|
|
||||||
var scheme = uri.Scheme;
|
|
||||||
var userInfo = uri.UserInfo;
|
|
||||||
var host = uri.Host;
|
|
||||||
var port = uri.Port;
|
|
||||||
var path = uri.PathAndQuery;
|
|
||||||
var parameters = uri.Parameters;
|
|
||||||
var fragment = uri.Fragment;
|
|
||||||
|
|
||||||
var queryString = default(HttpQueryNameValueCollection);
|
|
||||||
if (!parameters.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
queryString = new HttpQueryNameValueCollection(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentType = default(MediaTypeHeaderValue);
|
|
||||||
if (!contentType.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
contentType = default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddConfluenceServices();
|
|
||||||
services.AddModelContextProtocol();
|
|
||||||
services.AddOptions<WebApplication>();
|
|
||||||
|
|
||||||
services.AddOpenApiDocument(options =>
|
|
||||||
{
|
|
||||||
options.OpenApiDocumentPath = "/openapi.json";
|
|
||||||
options.DocumentName = "Confluence MCP";
|
|
||||||
options.DocumentDescription = "Confluence MCP Server";
|
|
||||||
options.DocumentVersion = "1.0";
|
|
||||||
|
|
||||||
options.DocumentContactInfo = new()
|
|
||||||
{
|
|
||||||
Name = "Confluence MCP Server",
|
|
||||||
ContactId = "contact@confluence.com"
|
|
||||||
};
|
|
||||||
|
|
||||||
options.DocumentLicenseInfo = new()
|
|
||||||
{
|
|
||||||
Name = "Apache 2.0",
|
|
||||||
Identifier = "Apache-2.0"
|
|
||||||
};
|
|
||||||
|
|
||||||
options.ClientId = "lazybear-confluence-mcp-client";
|
|
||||||
});
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
using LazyBear.Confluence;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace LazyBear.Confluence.Search;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public sealed class ConfluenceSearchTools(ConfluenceHttpClientProvider provider)
|
|
||||||
{
|
|
||||||
private readonly ConfluenceHttpClientProvider _provider = provider;
|
|
||||||
|
|
||||||
[McpServerTool, Description("Поиск страниц Confluence")]
|
|
||||||
public async Task<string> SearchPagesAsync(
|
|
||||||
[Description("Запрос")] string query,
|
|
||||||
[Description("Пространство")] string? spaceKey = null,
|
|
||||||
[Description("Типы")] string[]? types = null,
|
|
||||||
[Description("Максимум")] int? limit = 20,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(query))
|
|
||||||
{
|
|
||||||
return "query не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = "rest/api/cloud/search";
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, resource);
|
|
||||||
request.AddQueryParameter("cql", query);
|
|
||||||
request.AddQueryParameter("limit", limit?.ToString() ?? "20");
|
|
||||||
request.AddQueryParameter("spaceKeys", spaceKey ?? "ALL");
|
|
||||||
if (types != null)
|
|
||||||
{
|
|
||||||
foreach (var type in types)
|
|
||||||
{
|
|
||||||
request.AddQueryParameter("type", type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Поиск: {query} (spaces={spaceKey}, types={string.Join(",", types)})";
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Краулинг пространства Confluence")]
|
|
||||||
public async Task<string> CrawlSpaceAsync(
|
|
||||||
[Description("Пространство")] string spaceKey,
|
|
||||||
[Description("Максимум страниц")] int? limit = 100,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(spaceKey))
|
|
||||||
{
|
|
||||||
return "spaceKey не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
string CrawlPagesList(string spaceKey, int? limit)
|
|
||||||
{
|
|
||||||
return $"Краулинг пространства {spaceKey} с лимитом {limit?.ToString() ?? "100"}";
|
|
||||||
}
|
|
||||||
|
|
||||||
string CrawlResult(string crawlResult)
|
|
||||||
{
|
|
||||||
return $"Краулинг завершён. Результат: {crawlResult}";
|
|
||||||
}
|
|
||||||
|
|
||||||
var crawlResult = CrawlPagesList(spaceKey, limit);
|
|
||||||
return CrawlResult(crawlResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool, Description("Искать битые ссылки Confluence")]
|
|
||||||
public async Task<string> FindBrokenLinksAsync(
|
|
||||||
[Description("Пространство")] string spaceKey,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(spaceKey))
|
|
||||||
{
|
|
||||||
return "spaceKey не задан.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Поиск битых ссылок в {spaceKey}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"Confluence": {
|
|
||||||
"BaseUrl": "https://your-confluence-instance.atlassian.net/",
|
|
||||||
"Token": "your-token-here",
|
|
||||||
"DefaultSpace": "DEFAULT",
|
|
||||||
"HttpClient": {
|
|
||||||
"MaximumReadRevisions": 20,
|
|
||||||
"RequestBatchSize": 100,
|
|
||||||
"PageSize": 25,
|
|
||||||
"RequestTimeoutInSeconds": 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
245
__DONT_USE.md
245
__DONT_USE.md
@@ -1,245 +0,0 @@
|
|||||||
## AGENTS.md
|
|
||||||
|
|
||||||
### PRIORITY
|
|
||||||
|
|
||||||
1. User request
|
|
||||||
2. This file
|
|
||||||
3. Existing code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CODE
|
|
||||||
|
|
||||||
### STACK
|
|
||||||
|
|
||||||
* .NET / C#
|
|
||||||
* ASP.NET Core
|
|
||||||
* MCP
|
|
||||||
|
|
||||||
### STRUCTURE
|
|
||||||
|
|
||||||
* `Server/` — endpoints
|
|
||||||
* `Services/` — business logic
|
|
||||||
* `Tools/` — MCP tools
|
|
||||||
|
|
||||||
### RULES
|
|
||||||
|
|
||||||
**Before edit**
|
|
||||||
|
|
||||||
* Read related code
|
|
||||||
* Reuse existing patterns
|
|
||||||
* Do not over-engineer
|
|
||||||
|
|
||||||
**After edit**
|
|
||||||
|
|
||||||
* Run:
|
|
||||||
|
|
||||||
```
|
|
||||||
dotnet build
|
|
||||||
```
|
|
||||||
* Build must succeed
|
|
||||||
* Do not break MCP protocol
|
|
||||||
* Keep diff minimal
|
|
||||||
|
|
||||||
**Style**
|
|
||||||
|
|
||||||
* Match existing style
|
|
||||||
* Avoid duplication
|
|
||||||
* Prefer small changes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## COMMUNICATION
|
|
||||||
|
|
||||||
### LANGUAGE
|
|
||||||
|
|
||||||
* Output: Russian
|
|
||||||
* Code: English
|
|
||||||
* Comments/commits: Russian
|
|
||||||
|
|
||||||
### BEHAVIOR
|
|
||||||
|
|
||||||
* Be concise
|
|
||||||
* Do not explain obvious things
|
|
||||||
* Do not produce long texts
|
|
||||||
* Prefer action when no clarification is needed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### QUESTIONS
|
|
||||||
|
|
||||||
Default:
|
|
||||||
|
|
||||||
* If result can be improved by user choice → ASK FIRST
|
|
||||||
* Do not execute immediately if preferences affect result
|
|
||||||
|
|
||||||
Ask BEFORE action if:
|
|
||||||
|
|
||||||
* Multiple valid directions exist
|
|
||||||
* Result depends on user preference
|
|
||||||
* Request is broad (e.g. "suggest", "recommend", "generate")
|
|
||||||
|
|
||||||
Otherwise:
|
|
||||||
|
|
||||||
* Proceed with best reasonable assumption
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### QUESTION TOOL
|
|
||||||
|
|
||||||
`question` is the UI tool for user choices in OpenCode.
|
|
||||||
|
|
||||||
Use `question` BEFORE answering when:
|
|
||||||
|
|
||||||
* 2+ meaningful options exist
|
|
||||||
* clarification improves result quality
|
|
||||||
* choice affects architecture, config, data, or output
|
|
||||||
|
|
||||||
Do NOT skip `question` in these cases.
|
|
||||||
|
|
||||||
Do NOT use when:
|
|
||||||
|
|
||||||
* request is already specific
|
|
||||||
* only one valid answer exists
|
|
||||||
* clarification does not change result
|
|
||||||
|
|
||||||
If unavailable:
|
|
||||||
|
|
||||||
* ask in plain text
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### RESTRICTIONS
|
|
||||||
|
|
||||||
* Do not end with only a question
|
|
||||||
* Do not expose secrets
|
|
||||||
* Do not repeat user text
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TOOLS
|
|
||||||
|
|
||||||
Always assume tools MAY be available.
|
|
||||||
|
|
||||||
Before solving:
|
|
||||||
|
|
||||||
* Identify relevant tools
|
|
||||||
* Prefer tools when they simplify the task
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
* Do not invent tools
|
|
||||||
* Use only confirmed available tools
|
|
||||||
* If availability unclear → proceed without them
|
|
||||||
|
|
||||||
Tools are part of the solution, not optional.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MEMORY
|
|
||||||
|
|
||||||
Use ONLY if memory tools are available.
|
|
||||||
|
|
||||||
### READ FIRST
|
|
||||||
|
|
||||||
Before coding or assumptions:
|
|
||||||
|
|
||||||
1. Try `read_graph`
|
|
||||||
2. Then `search_nodes()`
|
|
||||||
3. Avoid duplicate observations
|
|
||||||
|
|
||||||
If unavailable:
|
|
||||||
|
|
||||||
* Skip memory usage
|
|
||||||
|
|
||||||
### KEY FORMAT
|
|
||||||
|
|
||||||
```text
|
|
||||||
lazybear/<type>/<name>
|
|
||||||
```
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
```text
|
|
||||||
lazybear/bug/auth-fail
|
|
||||||
lazybear/decision/mcp-timeout
|
|
||||||
lazybear/config/jira-base-url
|
|
||||||
```
|
|
||||||
|
|
||||||
### ALLOWED TYPES
|
|
||||||
|
|
||||||
* `architecture`
|
|
||||||
* `mcp_tool`
|
|
||||||
* `decision`
|
|
||||||
* `bug`
|
|
||||||
* `config`
|
|
||||||
* `task_log`
|
|
||||||
|
|
||||||
### WRITE ONLY WHEN USEFUL
|
|
||||||
|
|
||||||
* architecture changes → `architecture`
|
|
||||||
* new MCP tools → `mcp_tool`
|
|
||||||
* major decisions → `decision`
|
|
||||||
* important bugs → `bug`
|
|
||||||
* config changes → `config`
|
|
||||||
* completed non-trivial tasks → `task_log`
|
|
||||||
|
|
||||||
### RULES
|
|
||||||
|
|
||||||
* One entity = one type
|
|
||||||
* Keep entries short
|
|
||||||
* Do not duplicate
|
|
||||||
* Skip trivial changes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SECRETS
|
|
||||||
|
|
||||||
* Never print secrets
|
|
||||||
* Never commit `.env.local`
|
|
||||||
|
|
||||||
Use:
|
|
||||||
|
|
||||||
* `.env.local` → runtime
|
|
||||||
* `.env.example` → reference
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LINKS
|
|
||||||
|
|
||||||
Internal:
|
|
||||||
|
|
||||||
* Relative paths
|
|
||||||
* Spaces → `%20`
|
|
||||||
|
|
||||||
External:
|
|
||||||
|
|
||||||
* Markdown links only
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EDITING RULES
|
|
||||||
|
|
||||||
* Do not modify this file unless asked
|
|
||||||
* Do not change structure
|
|
||||||
* Keep instructions short and explicit
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CORE BEHAVIOR
|
|
||||||
|
|
||||||
* Ask first if it improves result quality
|
|
||||||
|
|
||||||
* Otherwise act
|
|
||||||
|
|
||||||
* Always consider tools before solving
|
|
||||||
|
|
||||||
* Prefer tools when useful
|
|
||||||
|
|
||||||
* Minimal changes only
|
|
||||||
|
|
||||||
* Do not invent tools
|
|
||||||
|
|
||||||
* Use tools only if confirmed available
|
|
||||||
|
|
||||||
* Never leak secrets
|
|
||||||
Reference in New Issue
Block a user