feat: внедрение RazorConsole TUI с runtime-управлением MCP-инструментами
- Добавлен RazorConsole.Core для интерактивного TUI-дашборда - ToolRegistryService: живое включение/отключение модулей и отдельных методов - InMemoryLogSink: кольцевой буфер логов с фильтрацией по модулю - TUI: 3 таба (Overview, Logs, Settings) - IToolModule: generic-интерфейс для легкого добавления новых MCP-модулей - Guard-проверка TryCheckEnabled() во всех существующих инструментах
This commit is contained in:
@@ -4,15 +4,17 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<!-- Включаем Razor-компоненты (Blazor-стиль) без MVC -->
|
||||||
|
<AddRazorSupportForMvc>false</AddRazorSupportForMvc>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="KubernetesClient" Version="19.0.2" />
|
<PackageReference Include="KubernetesClient" Version="19.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.AI" Version="10.4.1" />
|
<PackageReference Include="Microsoft.Extensions.AI" Version="10.4.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
|
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
|
||||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.2.0" />
|
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.2.0" />
|
||||||
<PackageReference Include="Polly" Version="8.4.2" />
|
<PackageReference Include="Polly" Version="8.4.2" />
|
||||||
|
<PackageReference Include="RazorConsole.Core" Version="0.5.0" />
|
||||||
<PackageReference Include="RestSharp" Version="112.0.0" />
|
<PackageReference Include="RestSharp" Version="112.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
using LazyBear.MCP.Services.Confluence;
|
using LazyBear.MCP.Services.Confluence;
|
||||||
using LazyBear.MCP.Services.Jira;
|
using LazyBear.MCP.Services.Jira;
|
||||||
using LazyBear.MCP.Services.Kubernetes;
|
using LazyBear.MCP.Services.Kubernetes;
|
||||||
|
using LazyBear.MCP.Services.Logging;
|
||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
|
using LazyBear.MCP.TUI;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// ── InMemoryLogSink: регистрируем Singleton и кастомный логгер ───────────────
|
||||||
|
var logSink = new InMemoryLogSink();
|
||||||
|
builder.Services.AddSingleton(logSink);
|
||||||
|
builder.Logging.AddProvider(new InMemoryLoggerProvider(logSink));
|
||||||
|
|
||||||
|
// ── MCP-провайдеры ───────────────────────────────────────────────────────────
|
||||||
builder.Services.AddSingleton<K8sClientProvider>();
|
builder.Services.AddSingleton<K8sClientProvider>();
|
||||||
builder.Services.AddSingleton<JiraClientProvider>();
|
builder.Services.AddSingleton<JiraClientProvider>();
|
||||||
builder.Services.AddSingleton<ConfluenceClientProvider>();
|
builder.Services.AddSingleton<ConfluenceClientProvider>();
|
||||||
|
|
||||||
|
// ── ToolRegistry ─────────────────────────────────────────────────────────────
|
||||||
|
builder.Services.AddSingleton<ToolRegistryService>();
|
||||||
|
|
||||||
|
// ── Модули инструментов (generic: добавь новый IToolModule — он появится в TUI)
|
||||||
|
builder.Services.AddSingleton<IToolModule, JiraToolModule>();
|
||||||
|
builder.Services.AddSingleton<IToolModule, KubernetesToolModule>();
|
||||||
|
builder.Services.AddSingleton<IToolModule, ConfluenceToolModule>();
|
||||||
|
|
||||||
|
// ── MCP-сервер ───────────────────────────────────────────────────────────────
|
||||||
builder.Services.AddMcpServer()
|
builder.Services.AddMcpServer()
|
||||||
.WithHttpTransport()
|
.WithHttpTransport()
|
||||||
.WithToolsFromAssembly();
|
.WithToolsFromAssembly();
|
||||||
|
|
||||||
|
// ── TUI как фоновый сервис ───────────────────────────────────────────────────
|
||||||
|
builder.Services.AddHostedService<TuiHostedService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.MapMcp();
|
app.MapMcp();
|
||||||
|
|
||||||
app.Run("http://localhost:5000");
|
app.Run("http://localhost:5000");
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
|
|
||||||
namespace LazyBear.MCP.Services.Confluence;
|
namespace LazyBear.MCP.Services.Confluence;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, IConfiguration configuration)
|
public sealed class ConfluencePagesTools(
|
||||||
|
ConfluenceClientProvider provider,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ToolRegistryService registry)
|
||||||
{
|
{
|
||||||
private readonly RestClient? _client = provider.Client;
|
private readonly RestClient? _client = provider.Client;
|
||||||
private readonly string? _clientInitializationError = provider.InitializationError;
|
private readonly string? _clientInitializationError = provider.InitializationError;
|
||||||
@@ -15,11 +19,26 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
|||||||
private readonly string _username = configuration["Confluence:Username"] ?? string.Empty;
|
private readonly string _username = configuration["Confluence:Username"] ?? string.Empty;
|
||||||
private readonly string _spaceKey = configuration["Confluence:SpaceKey"] ?? string.Empty;
|
private readonly string _spaceKey = configuration["Confluence:SpaceKey"] ?? string.Empty;
|
||||||
|
|
||||||
|
private const string ModuleName = "Confluence";
|
||||||
|
|
||||||
|
private bool TryCheckEnabled(string toolName, out string error)
|
||||||
|
{
|
||||||
|
if (!registry.IsToolEnabled(ModuleName, toolName))
|
||||||
|
{
|
||||||
|
error = $"Инструмент '{toolName}' модуля Confluence отключён в TUI.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
error = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Получить страницу Confluence по ID")]
|
[McpServerTool, Description("Получить страницу Confluence по ID")]
|
||||||
public async Task<string> GetPage(
|
public async Task<string> GetPage(
|
||||||
[Description("ID страницы Confluence")] string pageId,
|
[Description("ID страницы Confluence")] string pageId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("GetPage", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(pageId))
|
if (string.IsNullOrWhiteSpace(pageId))
|
||||||
{
|
{
|
||||||
return "ID страницы Confluence не задан.";
|
return "ID страницы Confluence не задан.";
|
||||||
@@ -64,6 +83,8 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
|||||||
[Description("Максимум страниц в ответе")] int maxResults = 20,
|
[Description("Максимум страниц в ответе")] int maxResults = 20,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("SearchPages", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (!TryGetClient(out var client, out var error))
|
if (!TryGetClient(out var client, out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
@@ -122,6 +143,8 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
|||||||
[Description("ID родительской страницы. Опционально")] string? parentId = null,
|
[Description("ID родительской страницы. Опционально")] string? parentId = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("CreatePage", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (!TryGetClient(out var client, out var error))
|
if (!TryGetClient(out var client, out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
@@ -188,6 +211,8 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
|||||||
[Description("Новое название страницы. Если пусто, не меняется")] string? title = null,
|
[Description("Новое название страницы. Если пусто, не меняется")] string? title = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("UpdatePage", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (!TryGetClient(out var client, out var error))
|
if (!TryGetClient(out var client, out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
@@ -250,6 +275,8 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
|||||||
[Description("ID страницы для удаления")] string pageId,
|
[Description("ID страницы для удаления")] string pageId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("DeletePage", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (!TryGetClient(out var client, out var error))
|
if (!TryGetClient(out var client, out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
@@ -279,6 +306,8 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
|||||||
[Description("Ключ пространства")] string spaceKey,
|
[Description("Ключ пространства")] string spaceKey,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("GetSpace", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (!TryGetClient(out var client, out var error))
|
if (!TryGetClient(out var client, out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
|
|||||||
19
LazyBear.MCP/Services/Confluence/ConfluenceToolModule.cs
Normal file
19
LazyBear.MCP/Services/Confluence/ConfluenceToolModule.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
|
|
||||||
|
namespace LazyBear.MCP.Services.Confluence;
|
||||||
|
|
||||||
|
public sealed class ConfluenceToolModule : IToolModule
|
||||||
|
{
|
||||||
|
public string ModuleName => "Confluence";
|
||||||
|
public string Description => "Confluence: страницы, пространства, поиск";
|
||||||
|
|
||||||
|
public IReadOnlyList<string> ToolNames =>
|
||||||
|
[
|
||||||
|
"GetPage",
|
||||||
|
"SearchPages",
|
||||||
|
"CreatePage",
|
||||||
|
"UpdatePage",
|
||||||
|
"DeletePage",
|
||||||
|
"GetSpace"
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,23 +1,42 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
|
|
||||||
namespace LazyBear.MCP.Services.Jira;
|
namespace LazyBear.MCP.Services.Jira;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration configuration)
|
public sealed class JiraIssueTools(
|
||||||
|
JiraClientProvider provider,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ToolRegistryService registry)
|
||||||
{
|
{
|
||||||
private readonly RestClient? _client = provider.Client;
|
private readonly RestClient? _client = provider.Client;
|
||||||
private readonly string? _clientInitializationError = provider.InitializationError;
|
private readonly string? _clientInitializationError = provider.InitializationError;
|
||||||
private readonly string _token = configuration["Jira:Token"] ?? string.Empty;
|
private readonly string _token = configuration["Jira:Token"] ?? string.Empty;
|
||||||
private readonly string _defaultProject = configuration["Jira:Project"] ?? string.Empty;
|
private readonly string _defaultProject = configuration["Jira:Project"] ?? string.Empty;
|
||||||
|
|
||||||
|
private const string ModuleName = "Jira";
|
||||||
|
|
||||||
|
private bool TryCheckEnabled(string toolName, out string error)
|
||||||
|
{
|
||||||
|
if (!registry.IsToolEnabled(ModuleName, toolName))
|
||||||
|
{
|
||||||
|
error = $"Инструмент '{toolName}' модуля Jira отключён в TUI.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
error = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Получить задачу Jira по ключу")]
|
[McpServerTool, Description("Получить задачу Jira по ключу")]
|
||||||
public async Task<string> GetIssue(
|
public async Task<string> GetIssue(
|
||||||
[Description("Ключ задачи, например PROJ-123")] string issueKey,
|
[Description("Ключ задачи, например PROJ-123")] string issueKey,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("GetIssue", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(issueKey))
|
if (string.IsNullOrWhiteSpace(issueKey))
|
||||||
{
|
{
|
||||||
return "Ключ задачи Jira не задан.";
|
return "Ключ задачи Jira не задан.";
|
||||||
@@ -60,6 +79,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
|||||||
[Description("Максимум задач в ответе")] int maxResults = 20,
|
[Description("Максимум задач в ответе")] int maxResults = 20,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("ListIssues", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (!TryGetClient(out var client, out var error))
|
if (!TryGetClient(out var client, out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
@@ -118,6 +139,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
|||||||
[Description("Описание задачи")] string? description = null,
|
[Description("Описание задачи")] string? description = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("CreateIssue", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (!TryGetClient(out var client, out var error))
|
if (!TryGetClient(out var client, out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
@@ -190,6 +213,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
|||||||
[Description("Новое описание")] string? description = null,
|
[Description("Новое описание")] string? description = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("UpdateIssue", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(summary) && string.IsNullOrWhiteSpace(description))
|
if (string.IsNullOrWhiteSpace(summary) && string.IsNullOrWhiteSpace(description))
|
||||||
{
|
{
|
||||||
return $"Нет полей для обновления задачи '{issueKey}'.";
|
return $"Нет полей для обновления задачи '{issueKey}'.";
|
||||||
@@ -256,6 +281,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
|||||||
[Description("Ключ задачи")] string issueKey,
|
[Description("Ключ задачи")] string issueKey,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("GetIssueStatuses", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (!TryGetClient(out var client, out var error))
|
if (!TryGetClient(out var client, out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
@@ -301,6 +328,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
|||||||
[Description("Максимум комментариев")] int limit = 20,
|
[Description("Максимум комментариев")] int limit = 20,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("ListIssueComments", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (!TryGetClient(out var client, out var error))
|
if (!TryGetClient(out var client, out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
@@ -349,6 +378,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
|||||||
[Description("Текст комментария")] string body,
|
[Description("Текст комментария")] string body,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("AddComment", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(body))
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
{
|
{
|
||||||
return "Текст комментария Jira не задан.";
|
return "Текст комментария Jira не задан.";
|
||||||
|
|||||||
20
LazyBear.MCP/Services/Jira/JiraToolModule.cs
Normal file
20
LazyBear.MCP/Services/Jira/JiraToolModule.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
|
|
||||||
|
namespace LazyBear.MCP.Services.Jira;
|
||||||
|
|
||||||
|
public sealed class JiraToolModule : IToolModule
|
||||||
|
{
|
||||||
|
public string ModuleName => "Jira";
|
||||||
|
public string Description => "Jira: задачи, комментарии, переходы статусов";
|
||||||
|
|
||||||
|
public IReadOnlyList<string> ToolNames =>
|
||||||
|
[
|
||||||
|
"GetIssue",
|
||||||
|
"ListIssues",
|
||||||
|
"CreateIssue",
|
||||||
|
"UpdateIssue",
|
||||||
|
"GetIssueStatuses",
|
||||||
|
"ListIssueComments",
|
||||||
|
"AddComment"
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using k8s;
|
using k8s;
|
||||||
using k8s.Autorest;
|
using k8s.Autorest;
|
||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
namespace LazyBear.MCP.Services.Kubernetes;
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfiguration configuration, ILogger<K8sConfigTools>? logger = null) : KubernetesToolsBase(clientProvider, configuration, logger)
|
public sealed class K8sConfigTools(
|
||||||
|
K8sClientProvider clientProvider,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ToolRegistryService registry,
|
||||||
|
ILogger<K8sConfigTools>? logger = null)
|
||||||
|
: KubernetesToolsBase(clientProvider, configuration, registry, logger)
|
||||||
{
|
{
|
||||||
private const int MaxSecretKeyLimit = 100;
|
private const int MaxSecretKeyLimit = 100;
|
||||||
|
|
||||||
@@ -15,6 +21,8 @@ public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfigurat
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("ListConfigMaps", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
@@ -52,6 +60,8 @@ public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfigurat
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("GetConfigMapData", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateResourceName(name, nameof(name));
|
ValidateResourceName(name, nameof(name));
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
@@ -83,6 +93,8 @@ public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfigurat
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("ListSecrets", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
@@ -120,6 +132,8 @@ public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfigurat
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("GetSecretKeys", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateResourceName(name, nameof(name));
|
ValidateResourceName(name, nameof(name));
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ using System.Text.Json;
|
|||||||
using k8s;
|
using k8s;
|
||||||
using k8s.Autorest;
|
using k8s.Autorest;
|
||||||
using k8s.Models;
|
using k8s.Models;
|
||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
namespace LazyBear.MCP.Services.Kubernetes;
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfiguration configuration, ILogger<K8sDeploymentTools>? logger = null) : KubernetesToolsBase(clientProvider, configuration, logger)
|
public sealed class K8sDeploymentTools(
|
||||||
|
K8sClientProvider clientProvider,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ToolRegistryService registry,
|
||||||
|
ILogger<K8sDeploymentTools>? logger = null)
|
||||||
|
: KubernetesToolsBase(clientProvider, configuration, registry, logger)
|
||||||
{
|
{
|
||||||
private const int MinReplicas = 0;
|
private const int MinReplicas = 0;
|
||||||
private const int MaxReplicas = 100;
|
private const int MaxReplicas = 100;
|
||||||
@@ -18,6 +24,8 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("ListDeployments", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
@@ -58,6 +66,8 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("ScaleDeployment", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateResourceName(name, nameof(name));
|
ValidateResourceName(name, nameof(name));
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
|
|
||||||
@@ -95,6 +105,8 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("GetRolloutStatus", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateResourceName(name, nameof(name));
|
ValidateResourceName(name, nameof(name));
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
@@ -126,6 +138,8 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("RestartDeployment", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateResourceName(name, nameof(name));
|
ValidateResourceName(name, nameof(name));
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using k8s;
|
using k8s;
|
||||||
using k8s.Autorest;
|
using k8s.Autorest;
|
||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
namespace LazyBear.MCP.Services.Kubernetes;
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class K8sNetworkTools(K8sClientProvider clientProvider, IConfiguration configuration, ILogger<K8sNetworkTools>? logger = null) : KubernetesToolsBase(clientProvider, configuration, logger)
|
public sealed class K8sNetworkTools(
|
||||||
|
K8sClientProvider clientProvider,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ToolRegistryService registry,
|
||||||
|
ILogger<K8sNetworkTools>? logger = null)
|
||||||
|
: KubernetesToolsBase(clientProvider, configuration, registry, logger)
|
||||||
{
|
{
|
||||||
[McpServerTool, Description("Список service в namespace")]
|
[McpServerTool, Description("Список service в namespace")]
|
||||||
public async Task<string> ListServices(
|
public async Task<string> ListServices(
|
||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("ListServices", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
@@ -55,6 +63,8 @@ public sealed class K8sNetworkTools(K8sClientProvider clientProvider, IConfigura
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("GetServiceDetails", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateResourceName(name, nameof(name));
|
ValidateResourceName(name, nameof(name));
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
@@ -90,6 +100,8 @@ public sealed class K8sNetworkTools(K8sClientProvider clientProvider, IConfigura
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("ListIngresses", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using k8s;
|
using k8s;
|
||||||
using k8s.Autorest;
|
using k8s.Autorest;
|
||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
namespace LazyBear.MCP.Services.Kubernetes;
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguration configuration, ILogger<K8sPodsTools>? logger = null) : KubernetesToolsBase(clientProvider, configuration, logger)
|
public sealed class K8sPodsTools(
|
||||||
|
K8sClientProvider clientProvider,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ToolRegistryService registry,
|
||||||
|
ILogger<K8sPodsTools>? logger = null)
|
||||||
|
: KubernetesToolsBase(clientProvider, configuration, registry, logger)
|
||||||
{
|
{
|
||||||
private const int MaxTailLines = 10;
|
private const int MaxTailLines = 10;
|
||||||
private const int MinTailLines = 10;
|
private const int MinTailLines = 10;
|
||||||
@@ -16,6 +22,8 @@ public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguratio
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("ListPods", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
if (!TryGetClient(out var client, out var clientError))
|
if (!TryGetClient(out var client, out var clientError))
|
||||||
@@ -55,6 +63,8 @@ public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguratio
|
|||||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("GetPodStatus", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateResourceName(name, nameof(name));
|
ValidateResourceName(name, nameof(name));
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
var ns = ResolveNamespace(@namespace);
|
var ns = ResolveNamespace(@namespace);
|
||||||
@@ -90,6 +100,8 @@ public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguratio
|
|||||||
int? tailLines = 100,
|
int? tailLines = 100,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!TryCheckEnabled("GetPodLogs", out var enabledError)) return enabledError;
|
||||||
|
|
||||||
ValidateResourceName(name, nameof(name));
|
ValidateResourceName(name, nameof(name));
|
||||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||||
|
|
||||||
|
|||||||
31
LazyBear.MCP/Services/Kubernetes/KubernetesToolModule.cs
Normal file
31
LazyBear.MCP/Services/Kubernetes/KubernetesToolModule.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
|
|
||||||
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
|
public sealed class KubernetesToolModule : IToolModule
|
||||||
|
{
|
||||||
|
public string ModuleName => "Kubernetes";
|
||||||
|
public string Description => "Kubernetes: поды, деплойменты, сервисы, конфиги";
|
||||||
|
|
||||||
|
public IReadOnlyList<string> ToolNames =>
|
||||||
|
[
|
||||||
|
// Pods
|
||||||
|
"ListPods",
|
||||||
|
"GetPodStatus",
|
||||||
|
"GetPodLogs",
|
||||||
|
// Deployments
|
||||||
|
"ListDeployments",
|
||||||
|
"ScaleDeployment",
|
||||||
|
"GetRolloutStatus",
|
||||||
|
"RestartDeployment",
|
||||||
|
// Network
|
||||||
|
"ListServices",
|
||||||
|
"GetServiceDetails",
|
||||||
|
"ListIngresses",
|
||||||
|
// Config
|
||||||
|
"ListConfigMaps",
|
||||||
|
"GetConfigMapData",
|
||||||
|
"ListSecrets",
|
||||||
|
"GetSecretKeys"
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,18 +1,36 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using k8s;
|
using k8s;
|
||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
namespace LazyBear.MCP.Services.Kubernetes;
|
namespace LazyBear.MCP.Services.Kubernetes;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public abstract class KubernetesToolsBase(K8sClientProvider clientProvider, IConfiguration configuration, ILogger? logger = null)
|
public abstract class KubernetesToolsBase(
|
||||||
|
K8sClientProvider clientProvider,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ToolRegistryService registry,
|
||||||
|
ILogger? logger = null)
|
||||||
{
|
{
|
||||||
protected readonly IKubernetes? _client = clientProvider.Client;
|
protected readonly IKubernetes? _client = clientProvider.Client;
|
||||||
protected readonly string? _clientInitializationError = clientProvider.InitializationError;
|
protected readonly string? _clientInitializationError = clientProvider.InitializationError;
|
||||||
protected readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default";
|
protected readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default";
|
||||||
protected readonly ILogger? _logger = logger;
|
protected readonly ILogger? _logger = logger;
|
||||||
|
protected readonly ToolRegistryService _registry = registry;
|
||||||
|
|
||||||
|
protected const string K8sModuleName = "Kubernetes";
|
||||||
|
|
||||||
|
protected bool TryCheckEnabled(string toolName, out string error)
|
||||||
|
{
|
||||||
|
if (!_registry.IsToolEnabled(K8sModuleName, toolName))
|
||||||
|
{
|
||||||
|
error = $"Инструмент '{toolName}' модуля Kubernetes отключён в TUI.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
error = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected static readonly Regex NamespaceRegex = new(@"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
protected static readonly Regex NamespaceRegex = new(@"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
||||||
protected static readonly Regex ResourceNameRegex = new(@"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
protected static readonly Regex ResourceNameRegex = new(@"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
||||||
|
|||||||
47
LazyBear.MCP/Services/Logging/InMemoryLogSink.cs
Normal file
47
LazyBear.MCP/Services/Logging/InMemoryLogSink.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace LazyBear.MCP.Services.Logging;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Singleton, хранящий последние <see cref="Capacity"/> записей лога в памяти.
|
||||||
|
/// Безопасен для многопоточной записи.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InMemoryLogSink
|
||||||
|
{
|
||||||
|
public const int Capacity = 500;
|
||||||
|
|
||||||
|
private readonly ConcurrentQueue<LogEntry> _entries = new();
|
||||||
|
|
||||||
|
/// <summary>Событие вызывается при добавлении новой записи.</summary>
|
||||||
|
public event Action<LogEntry>? OnLog;
|
||||||
|
|
||||||
|
public void Add(LogEntry entry)
|
||||||
|
{
|
||||||
|
_entries.Enqueue(entry);
|
||||||
|
|
||||||
|
// Обрезаем старые записи
|
||||||
|
while (_entries.Count > Capacity)
|
||||||
|
{
|
||||||
|
_entries.TryDequeue(out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLog?.Invoke(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает снимок всех записей. Опциональный фильтр по категории-префиксу.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<LogEntry> GetEntries(string? categoryPrefix = null)
|
||||||
|
{
|
||||||
|
var all = _entries.ToArray();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(categoryPrefix))
|
||||||
|
{
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.FindAll(all, e => e.Category.StartsWith(categoryPrefix, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() => _entries.Clear();
|
||||||
|
}
|
||||||
42
LazyBear.MCP/Services/Logging/InMemoryLoggerProvider.cs
Normal file
42
LazyBear.MCP/Services/Logging/InMemoryLoggerProvider.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace LazyBear.MCP.Services.Logging;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ILoggerProvider, направляющий все логи в <see cref="InMemoryLogSink"/>.
|
||||||
|
/// </summary>
|
||||||
|
[ProviderAlias("InMemory")]
|
||||||
|
public sealed class InMemoryLoggerProvider(InMemoryLogSink sink) : ILoggerProvider
|
||||||
|
{
|
||||||
|
public ILogger CreateLogger(string categoryName) =>
|
||||||
|
new InMemoryLogger(sink, categoryName);
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class InMemoryLogger(InMemoryLogSink sink, string category) : ILogger
|
||||||
|
{
|
||||||
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||||
|
|
||||||
|
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
|
||||||
|
|
||||||
|
public void Log<TState>(
|
||||||
|
LogLevel logLevel,
|
||||||
|
EventId eventId,
|
||||||
|
TState state,
|
||||||
|
Exception? exception,
|
||||||
|
Func<TState, Exception?, string> formatter)
|
||||||
|
{
|
||||||
|
if (!IsEnabled(logLevel)) return;
|
||||||
|
|
||||||
|
var message = formatter(state, exception);
|
||||||
|
var entry = new LogEntry(
|
||||||
|
DateTimeOffset.Now,
|
||||||
|
logLevel,
|
||||||
|
category,
|
||||||
|
message,
|
||||||
|
exception?.ToString());
|
||||||
|
|
||||||
|
sink.Add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
LazyBear.MCP/Services/Logging/LogEntry.cs
Normal file
14
LazyBear.MCP/Services/Logging/LogEntry.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace LazyBear.MCP.Services.Logging;
|
||||||
|
|
||||||
|
public sealed record LogEntry(
|
||||||
|
DateTimeOffset Timestamp,
|
||||||
|
LogLevel Level,
|
||||||
|
string Category,
|
||||||
|
string Message,
|
||||||
|
string? Exception = null)
|
||||||
|
{
|
||||||
|
/// <summary>Короткое имя категории (последний сегмент namespace)</summary>
|
||||||
|
public string ShortCategory => Category.Contains('.')
|
||||||
|
? Category[(Category.LastIndexOf('.') + 1)..]
|
||||||
|
: Category;
|
||||||
|
}
|
||||||
17
LazyBear.MCP/Services/ToolRegistry/IToolModule.cs
Normal file
17
LazyBear.MCP/Services/ToolRegistry/IToolModule.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace LazyBear.MCP.Services.ToolRegistry;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Описывает группу MCP-инструментов (один интеграционный модуль).
|
||||||
|
/// Реализуйте этот интерфейс для регистрации новых модулей.
|
||||||
|
/// </summary>
|
||||||
|
public interface IToolModule
|
||||||
|
{
|
||||||
|
/// <summary>Уникальное имя модуля (Jira, Kubernetes, Confluence, …)</summary>
|
||||||
|
string ModuleName { get; }
|
||||||
|
|
||||||
|
/// <summary>Имена всех инструментов, входящих в модуль.</summary>
|
||||||
|
IReadOnlyList<string> ToolNames { get; }
|
||||||
|
|
||||||
|
/// <summary>Человекочитаемое описание модуля для TUI.</summary>
|
||||||
|
string Description { get; }
|
||||||
|
}
|
||||||
96
LazyBear.MCP/Services/ToolRegistry/ToolRegistryService.cs
Normal file
96
LazyBear.MCP/Services/ToolRegistry/ToolRegistryService.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace LazyBear.MCP.Services.ToolRegistry;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Singleton. Хранит runtime-состояние включённости модулей и отдельных инструментов.
|
||||||
|
/// Thread-safe. Уведомляет подписчиков при любом изменении.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ToolRegistryService
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, bool> _moduleEnabled = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly ConcurrentDictionary<string, bool> _toolEnabled = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly List<IToolModule> _modules = [];
|
||||||
|
private readonly Lock _modulesLock = new();
|
||||||
|
|
||||||
|
/// <summary>Вызывается при любом изменении состояния.</summary>
|
||||||
|
public event Action? StateChanged;
|
||||||
|
|
||||||
|
// ── Регистрация ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void RegisterModule(IToolModule module)
|
||||||
|
{
|
||||||
|
lock (_modulesLock)
|
||||||
|
{
|
||||||
|
if (_modules.Any(m => string.Equals(m.ModuleName, module.ModuleName, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_modules.Add(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
_moduleEnabled.TryAdd(module.ModuleName, true);
|
||||||
|
|
||||||
|
foreach (var tool in module.ToolNames)
|
||||||
|
{
|
||||||
|
_toolEnabled.TryAdd(MakeKey(module.ModuleName, tool), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Запросы состояния ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public IReadOnlyList<IToolModule> GetModules()
|
||||||
|
{
|
||||||
|
lock (_modulesLock)
|
||||||
|
{
|
||||||
|
return _modules.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsModuleEnabled(string moduleName) =>
|
||||||
|
_moduleEnabled.GetValueOrDefault(moduleName, true);
|
||||||
|
|
||||||
|
public bool IsToolEnabled(string moduleName, string toolName) =>
|
||||||
|
IsModuleEnabled(moduleName) &&
|
||||||
|
_toolEnabled.GetValueOrDefault(MakeKey(moduleName, toolName), true);
|
||||||
|
|
||||||
|
// ── Переключение ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void SetModuleEnabled(string moduleName, bool enabled)
|
||||||
|
{
|
||||||
|
_moduleEnabled[moduleName] = enabled;
|
||||||
|
StateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetToolEnabled(string moduleName, string toolName, bool enabled)
|
||||||
|
{
|
||||||
|
_toolEnabled[MakeKey(moduleName, toolName)] = enabled;
|
||||||
|
StateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToggleModule(string moduleName) =>
|
||||||
|
SetModuleEnabled(moduleName, !IsModuleEnabled(moduleName));
|
||||||
|
|
||||||
|
public void ToggleTool(string moduleName, string toolName) =>
|
||||||
|
SetToolEnabled(moduleName, toolName, !IsToolEnabled(moduleName, toolName));
|
||||||
|
|
||||||
|
// ── Счётчики для Overview ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public (int Active, int Total) GetToolCounts(string moduleName)
|
||||||
|
{
|
||||||
|
lock (_modulesLock)
|
||||||
|
{
|
||||||
|
var module = _modules.FirstOrDefault(m =>
|
||||||
|
string.Equals(m.ModuleName, moduleName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (module is null) return (0, 0);
|
||||||
|
|
||||||
|
var total = module.ToolNames.Count;
|
||||||
|
var active = module.ToolNames.Count(t => IsToolEnabled(moduleName, t));
|
||||||
|
return (active, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static string MakeKey(string module, string tool) => $"{module}::{tool}";
|
||||||
|
}
|
||||||
81
LazyBear.MCP/TUI/Components/App.razor
Normal file
81
LazyBear.MCP/TUI/Components/App.razor
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
@using LazyBear.MCP.Services.Logging
|
||||||
|
@using LazyBear.MCP.Services.ToolRegistry
|
||||||
|
@inject ToolRegistryService Registry
|
||||||
|
@inject InMemoryLogSink LogSink
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
<Rows>
|
||||||
|
<Panel Title="LazyBear MCP" BorderColor="@Spectre.Console.Color.Gold1" Expand="true">
|
||||||
|
<Rows>
|
||||||
|
@* Таб-навигация *@
|
||||||
|
<Columns>
|
||||||
|
<TextButton Content="[1] Overview"
|
||||||
|
OnClick="@(() => SetTab(Tab.Overview))"
|
||||||
|
BackgroundColor="@(_activeTab == Tab.Overview ? Spectre.Console.Color.DarkBlue : Spectre.Console.Color.Grey23)"
|
||||||
|
FocusedColor="@Spectre.Console.Color.Blue"
|
||||||
|
FocusOrder="1" />
|
||||||
|
<TextButton Content="[2] Logs"
|
||||||
|
OnClick="@(() => SetTab(Tab.Logs))"
|
||||||
|
BackgroundColor="@(_activeTab == Tab.Logs ? Spectre.Console.Color.DarkBlue : Spectre.Console.Color.Grey23)"
|
||||||
|
FocusedColor="@Spectre.Console.Color.Blue"
|
||||||
|
FocusOrder="2" />
|
||||||
|
<TextButton Content="[3] Settings"
|
||||||
|
OnClick="@(() => SetTab(Tab.Settings))"
|
||||||
|
BackgroundColor="@(_activeTab == Tab.Settings ? Spectre.Console.Color.DarkBlue : Spectre.Console.Color.Grey23)"
|
||||||
|
FocusedColor="@Spectre.Console.Color.Blue"
|
||||||
|
FocusOrder="3" />
|
||||||
|
</Columns>
|
||||||
|
|
||||||
|
@* Контент таба *@
|
||||||
|
@if (_activeTab == Tab.Overview)
|
||||||
|
{
|
||||||
|
<OverviewTab />
|
||||||
|
}
|
||||||
|
else if (_activeTab == Tab.Logs)
|
||||||
|
{
|
||||||
|
<LogsTab />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<SettingsTab />
|
||||||
|
}
|
||||||
|
</Rows>
|
||||||
|
</Panel>
|
||||||
|
</Rows>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private enum Tab { Overview, Logs, Settings }
|
||||||
|
private Tab _activeTab = Tab.Overview;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Registry.StateChanged += OnStateChanged;
|
||||||
|
LogSink.OnLog += OnNewLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetTab(Tab tab)
|
||||||
|
{
|
||||||
|
_activeTab = tab;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStateChanged()
|
||||||
|
{
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNewLog(LogEntry _)
|
||||||
|
{
|
||||||
|
if (_activeTab == Tab.Logs)
|
||||||
|
{
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Registry.StateChanged -= OnStateChanged;
|
||||||
|
LogSink.OnLog -= OnNewLog;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
LazyBear.MCP/TUI/Components/LogsTab.razor
Normal file
119
LazyBear.MCP/TUI/Components/LogsTab.razor
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
@using LazyBear.MCP.Services.Logging
|
||||||
|
@inject InMemoryLogSink LogSink
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
<Rows>
|
||||||
|
<Markup Content=" " />
|
||||||
|
|
||||||
|
@* Фильтр по модулю *@
|
||||||
|
<Columns>
|
||||||
|
<Markup Content="Filter: " />
|
||||||
|
<Select TItem="string"
|
||||||
|
Options="@_filterOptions"
|
||||||
|
Value="@_selectedFilter"
|
||||||
|
ValueChanged="@OnFilterChanged"
|
||||||
|
FocusOrder="10" />
|
||||||
|
</Columns>
|
||||||
|
|
||||||
|
<Markup Content=" " />
|
||||||
|
|
||||||
|
@{
|
||||||
|
var entries = GetFilteredEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (entries.Count == 0)
|
||||||
|
{
|
||||||
|
<Markup Content="[grey]No log entries yet...[/]" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@* Показываем последние 20 строк с прокруткой *@
|
||||||
|
<ViewHeightScrollable LinesToRender="20"
|
||||||
|
ScrollOffset="@_scrollOffset"
|
||||||
|
ScrollOffsetChanged="@(v => { _scrollOffset = v; })" >
|
||||||
|
<Rows>
|
||||||
|
@foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
var levelColor = entry.Level switch
|
||||||
|
{
|
||||||
|
LogLevel.Error or LogLevel.Critical => Spectre.Console.Color.Red,
|
||||||
|
LogLevel.Warning => Spectre.Console.Color.Yellow,
|
||||||
|
LogLevel.Information => Spectre.Console.Color.White,
|
||||||
|
_ => Spectre.Console.Color.Grey
|
||||||
|
};
|
||||||
|
|
||||||
|
var levelTag = entry.Level switch
|
||||||
|
{
|
||||||
|
LogLevel.Error => "ERR",
|
||||||
|
LogLevel.Critical => "CRT",
|
||||||
|
LogLevel.Warning => "WRN",
|
||||||
|
LogLevel.Information => "INF",
|
||||||
|
LogLevel.Debug => "DBG",
|
||||||
|
_ => "TRC"
|
||||||
|
};
|
||||||
|
|
||||||
|
var time = entry.Timestamp.ToString("HH:mm:ss");
|
||||||
|
var cat = entry.ShortCategory.Length > 18
|
||||||
|
? entry.ShortCategory[..18]
|
||||||
|
: entry.ShortCategory.PadRight(18);
|
||||||
|
var msg = entry.Message.Length > 80
|
||||||
|
? entry.Message[..80] + "..."
|
||||||
|
: entry.Message;
|
||||||
|
|
||||||
|
<Columns>
|
||||||
|
<Markup Content="@($"[grey]{time}[/]")" />
|
||||||
|
<Markup Content="@($" {levelTag} ")" Foreground="@levelColor" />
|
||||||
|
<Markup Content="@($"[grey]{cat}[/]")" />
|
||||||
|
<Markup Content="@($" {msg}")" />
|
||||||
|
</Columns>
|
||||||
|
}
|
||||||
|
</Rows>
|
||||||
|
</ViewHeightScrollable>
|
||||||
|
}
|
||||||
|
</Rows>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string _selectedFilter = "All";
|
||||||
|
private int _scrollOffset = 0;
|
||||||
|
|
||||||
|
private string[] _filterOptions = ["All", "Jira", "Kubernetes", "Confluence", "MCP", "System"];
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string?> FilterPrefixes = new()
|
||||||
|
{
|
||||||
|
["All"] = null,
|
||||||
|
["Jira"] = "LazyBear.MCP.Services.Jira",
|
||||||
|
["Kubernetes"] = "LazyBear.MCP.Services.Kubernetes",
|
||||||
|
["Confluence"] = "LazyBear.MCP.Services.Confluence",
|
||||||
|
["MCP"] = "ModelContextProtocol",
|
||||||
|
["System"] = "Microsoft"
|
||||||
|
};
|
||||||
|
|
||||||
|
private IReadOnlyList<LogEntry> GetFilteredEntries()
|
||||||
|
{
|
||||||
|
FilterPrefixes.TryGetValue(_selectedFilter, out var prefix);
|
||||||
|
return LogSink.GetEntries(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFilterChanged(string value)
|
||||||
|
{
|
||||||
|
_selectedFilter = value;
|
||||||
|
_scrollOffset = 0;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
LogSink.OnLog += HandleNewLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleNewLog(LogEntry _)
|
||||||
|
{
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
LogSink.OnLog -= HandleNewLog;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
LazyBear.MCP/TUI/Components/OverviewTab.razor
Normal file
39
LazyBear.MCP/TUI/Components/OverviewTab.razor
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
@using LazyBear.MCP.Services.ToolRegistry
|
||||||
|
@inject ToolRegistryService Registry
|
||||||
|
|
||||||
|
<Rows>
|
||||||
|
<Markup Content=" " />
|
||||||
|
@foreach (var module in Registry.GetModules())
|
||||||
|
{
|
||||||
|
var (active, total) = Registry.GetToolCounts(module.ModuleName);
|
||||||
|
var isEnabled = Registry.IsModuleEnabled(module.ModuleName);
|
||||||
|
var statusColor = isEnabled ? Spectre.Console.Color.Green : Spectre.Console.Color.Red;
|
||||||
|
var statusText = isEnabled ? "ENABLED" : "DISABLED";
|
||||||
|
var activeColor = active == total
|
||||||
|
? Spectre.Console.Color.Green
|
||||||
|
: (active == 0 ? Spectre.Console.Color.Red : Spectre.Console.Color.Yellow);
|
||||||
|
|
||||||
|
<Panel Title="@module.ModuleName"
|
||||||
|
BorderColor="@(isEnabled ? Spectre.Console.Color.Green3 : Spectre.Console.Color.Grey46)"
|
||||||
|
Expand="true">
|
||||||
|
<Columns>
|
||||||
|
<Rows>
|
||||||
|
<Columns>
|
||||||
|
<Markup Content="Status: " />
|
||||||
|
<Markup Content="@statusText" Foreground="@statusColor" />
|
||||||
|
</Columns>
|
||||||
|
<Columns>
|
||||||
|
<Markup Content="Tools: " />
|
||||||
|
<Markup Content="@($"{active}/{total} active")" Foreground="@activeColor" />
|
||||||
|
</Columns>
|
||||||
|
<Markup Content="@module.Description" Foreground="@Spectre.Console.Color.Grey" />
|
||||||
|
</Rows>
|
||||||
|
</Columns>
|
||||||
|
</Panel>
|
||||||
|
}
|
||||||
|
<Markup Content=" " />
|
||||||
|
<Markup Content="[grey]Go to Settings tab to toggle modules and tools[/]" />
|
||||||
|
</Rows>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
}
|
||||||
42
LazyBear.MCP/TUI/Components/SettingsTab.razor
Normal file
42
LazyBear.MCP/TUI/Components/SettingsTab.razor
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
@using LazyBear.MCP.Services.ToolRegistry
|
||||||
|
@inject ToolRegistryService Registry
|
||||||
|
|
||||||
|
<Rows>
|
||||||
|
<Markup Content=" " />
|
||||||
|
<Markup Content="[bold]Tool Registry — runtime enable/disable[/]" />
|
||||||
|
<Markup Content="[grey]Changes take effect immediately without restart[/]" />
|
||||||
|
<Markup Content=" " />
|
||||||
|
|
||||||
|
@{
|
||||||
|
int focusIdx = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach (var module in Registry.GetModules())
|
||||||
|
{
|
||||||
|
var moduleEnabled = Registry.IsModuleEnabled(module.ModuleName);
|
||||||
|
var moduleColor = moduleEnabled ? Spectre.Console.Color.Green : Spectre.Console.Color.Red;
|
||||||
|
var moduleName = module.ModuleName;
|
||||||
|
var capturedFocus = focusIdx++;
|
||||||
|
|
||||||
|
<Panel Title="@module.ModuleName" BorderColor="@moduleColor" Expand="true">
|
||||||
|
<Rows>
|
||||||
|
<Columns>
|
||||||
|
<TextButton Content="@(moduleEnabled ? "[green]■ MODULE ENABLED[/]" : "[red]□ MODULE DISABLED[/]")"
|
||||||
|
OnClick="@(() => Registry.ToggleModule(moduleName))"
|
||||||
|
BackgroundColor="@(moduleEnabled ? Spectre.Console.Color.DarkGreen : Spectre.Console.Color.DarkRed)"
|
||||||
|
FocusedColor="@Spectre.Console.Color.Yellow"
|
||||||
|
FocusOrder="@capturedFocus" />
|
||||||
|
<Markup Content="@($" {module.Description}")" Foreground="@Spectre.Console.Color.Grey" />
|
||||||
|
</Columns>
|
||||||
|
<Markup Content=" " />
|
||||||
|
<ToolButtonList Module="@module" StartFocusIdx="@focusIdx" />
|
||||||
|
</Rows>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
focusIdx += module.ToolNames.Count;
|
||||||
|
<Markup Content=" " />
|
||||||
|
}
|
||||||
|
</Rows>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
}
|
||||||
21
LazyBear.MCP/TUI/Components/ToolButtonList.razor
Normal file
21
LazyBear.MCP/TUI/Components/ToolButtonList.razor
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
@using LazyBear.MCP.Services.ToolRegistry
|
||||||
|
@inject ToolRegistryService Registry
|
||||||
|
|
||||||
|
@foreach (var (tool, idx) in Module.ToolNames.Select((t, i) => (t, i)))
|
||||||
|
{
|
||||||
|
var toolEnabled = Registry.IsToolEnabled(Module.ModuleName, tool);
|
||||||
|
var toolName = tool;
|
||||||
|
var moduleName = Module.ModuleName;
|
||||||
|
var fo = StartFocusIdx + idx;
|
||||||
|
|
||||||
|
<TextButton Content="@(toolEnabled ? $"[green]✓[/] {toolName}" : $"[grey]✗[/] {toolName}")"
|
||||||
|
OnClick="@(() => Registry.ToggleTool(moduleName, toolName))"
|
||||||
|
BackgroundColor="@(toolEnabled ? Spectre.Console.Color.Grey19 : Spectre.Console.Color.Grey7)"
|
||||||
|
FocusedColor="@Spectre.Console.Color.Yellow"
|
||||||
|
FocusOrder="@fo" />
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter, EditorRequired] public IToolModule Module { get; set; } = null!;
|
||||||
|
[Parameter] public int StartFocusIdx { get; set; } = 100;
|
||||||
|
}
|
||||||
5
LazyBear.MCP/TUI/Components/_Imports.razor
Normal file
5
LazyBear.MCP/TUI/Components/_Imports.razor
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components
|
||||||
|
@using RazorConsole.Components
|
||||||
|
@using RazorConsole.Core
|
||||||
|
@using RazorConsole.Core.Rendering
|
||||||
|
@using LazyBear.MCP.TUI.Components
|
||||||
69
LazyBear.MCP/TUI/TuiHostedService.cs
Normal file
69
LazyBear.MCP/TUI/TuiHostedService.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using LazyBear.MCP.Services.ToolRegistry;
|
||||||
|
using LazyBear.MCP.TUI.Components;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using RazorConsole.Core;
|
||||||
|
|
||||||
|
namespace LazyBear.MCP.TUI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Запускает RazorConsole TUI как IHostedService в отдельном потоке,
|
||||||
|
/// чтобы не блокировать ASP.NET Core pipeline.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TuiHostedService(IServiceProvider services, ILogger<TuiHostedService> logger) : IHostedService
|
||||||
|
{
|
||||||
|
private Thread? _tuiThread;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
// Регистрируем все IToolModule-модули в ToolRegistryService
|
||||||
|
var registry = services.GetRequiredService<ToolRegistryService>();
|
||||||
|
foreach (var module in services.GetServices<IToolModule>())
|
||||||
|
{
|
||||||
|
registry.RegisterModule(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tuiThread = new Thread(RunTui)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "RazorConsole-TUI"
|
||||||
|
};
|
||||||
|
_tuiThread.Start();
|
||||||
|
|
||||||
|
logger.LogInformation("TUI запущен в фоновом потоке");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
logger.LogInformation("TUI остановлен");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunTui()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var host = Host.CreateDefaultBuilder()
|
||||||
|
.UseRazorConsole<App>(configure: configure =>
|
||||||
|
{
|
||||||
|
configure.ConfigureServices((_, svc) =>
|
||||||
|
{
|
||||||
|
// Пробрасываем ключевые Singleton из основного DI-контейнера в TUI-контейнер
|
||||||
|
svc.AddSingleton(services.GetRequiredService<ToolRegistryService>());
|
||||||
|
svc.AddSingleton(services.GetRequiredService<Services.Logging.InMemoryLogSink>());
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
host.Run();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Ошибка в потоке TUI");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user