Чистка

This commit is contained in:
2026-04-13 15:05:08 +03:00
parent 1c7368de5b
commit aa124b98af
12 changed files with 0 additions and 1248 deletions

View File

@@ -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() }
};
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 + "]";
}
}

View File

@@ -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}";
}
}

View File

@@ -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!");
}

View File

@@ -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";
});

View File

@@ -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}";
}
}

View File

@@ -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
}
}
}

View File

@@ -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