Confluence init
This commit is contained in:
154
Libraries/Confluence/ConfluenceClientFactory.cs
Normal file
154
Libraries/Confluence/ConfluenceClientFactory.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
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() }
|
||||
};
|
||||
}
|
||||
}
|
||||
231
Libraries/Confluence/ConfluenceCloudTools.cs
Normal file
231
Libraries/Confluence/ConfluenceCloudTools.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
101
Libraries/Confluence/ConfluenceCommentsTools.cs
Normal file
101
Libraries/Confluence/ConfluenceCommentsTools.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
160
Libraries/Confluence/ConfluenceDataCenterTools.cs
Normal file
160
Libraries/Confluence/ConfluenceDataCenterTools.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
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;
|
||||
}
|
||||
38
Libraries/Confluence/ConfluenceModels.cs
Normal file
38
Libraries/Confluence/ConfluenceModels.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
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;
|
||||
}
|
||||
52
Libraries/Confluence/ConfluenceSpacesTools.cs
Normal file
52
Libraries/Confluence/ConfluenceSpacesTools.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
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 + "]";
|
||||
}
|
||||
}
|
||||
88
Libraries/Confluence/Pages/ConfluencePagesTools.cs
Normal file
88
Libraries/Confluence/Pages/ConfluencePagesTools.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
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}";
|
||||
}
|
||||
}
|
||||
17
Libraries/Confluence/Pages/find-files.ps1
Normal file
17
Libraries/Confluence/Pages/find-files.ps1
Normal file
@@ -0,0 +1,17 @@
|
||||
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!");
|
||||
}
|
||||
72
Libraries/Confluence/Program.cs
Normal file
72
Libraries/Confluence/Program.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
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";
|
||||
});
|
||||
77
Libraries/Confluence/Search/ConfluenceSearchTools.cs
Normal file
77
Libraries/Confluence/Search/ConfluenceSearchTools.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
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}";
|
||||
}
|
||||
}
|
||||
13
Libraries/Confluence/appsettings.json
Normal file
13
Libraries/Confluence/appsettings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Confluence": {
|
||||
"BaseUrl": "https://your-confluence-instance.atlassian.net/",
|
||||
"Token": "your-token-here",
|
||||
"DefaultSpace": "DEFAULT",
|
||||
"HttpClient": {
|
||||
"MaximumReadRevisions": 20,
|
||||
"RequestBatchSize": 100,
|
||||
"PageSize": 25,
|
||||
"RequestTimeoutInSeconds": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user