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:
@@ -1,13 +1,17 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LazyBear.MCP.Services.ToolRegistry;
|
||||
using ModelContextProtocol.Server;
|
||||
using RestSharp;
|
||||
|
||||
namespace LazyBear.MCP.Services.Confluence;
|
||||
|
||||
[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 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 _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")]
|
||||
public async Task<string> GetPage(
|
||||
[Description("ID страницы Confluence")] string pageId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("GetPage", out var enabledError)) return enabledError;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pageId))
|
||||
{
|
||||
return "ID страницы Confluence не задан.";
|
||||
@@ -64,6 +83,8 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
||||
[Description("Максимум страниц в ответе")] int maxResults = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("SearchPages", out var enabledError)) return enabledError;
|
||||
|
||||
if (!TryGetClient(out var client, out var error))
|
||||
{
|
||||
return error;
|
||||
@@ -122,6 +143,8 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
||||
[Description("ID родительской страницы. Опционально")] string? parentId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("CreatePage", out var enabledError)) return enabledError;
|
||||
|
||||
if (!TryGetClient(out var client, out var error))
|
||||
{
|
||||
return error;
|
||||
@@ -188,6 +211,8 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
||||
[Description("Новое название страницы. Если пусто, не меняется")] string? title = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("UpdatePage", out var enabledError)) return enabledError;
|
||||
|
||||
if (!TryGetClient(out var client, out var error))
|
||||
{
|
||||
return error;
|
||||
@@ -250,6 +275,8 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
||||
[Description("ID страницы для удаления")] string pageId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("DeletePage", out var enabledError)) return enabledError;
|
||||
|
||||
if (!TryGetClient(out var client, out var error))
|
||||
{
|
||||
return error;
|
||||
@@ -279,6 +306,8 @@ public sealed class ConfluencePagesTools(ConfluenceClientProvider provider, ICon
|
||||
[Description("Ключ пространства")] string spaceKey,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("GetSpace", out var enabledError)) return enabledError;
|
||||
|
||||
if (!TryGetClient(out var client, out var 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.Text.Json;
|
||||
using LazyBear.MCP.Services.ToolRegistry;
|
||||
using ModelContextProtocol.Server;
|
||||
using RestSharp;
|
||||
|
||||
namespace LazyBear.MCP.Services.Jira;
|
||||
|
||||
[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 string? _clientInitializationError = provider.InitializationError;
|
||||
private readonly string _token = configuration["Jira:Token"] ?? 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 по ключу")]
|
||||
public async Task<string> GetIssue(
|
||||
[Description("Ключ задачи, например PROJ-123")] string issueKey,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("GetIssue", out var enabledError)) return enabledError;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(issueKey))
|
||||
{
|
||||
return "Ключ задачи Jira не задан.";
|
||||
@@ -60,6 +79,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
||||
[Description("Максимум задач в ответе")] int maxResults = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("ListIssues", out var enabledError)) return enabledError;
|
||||
|
||||
if (!TryGetClient(out var client, out var error))
|
||||
{
|
||||
return error;
|
||||
@@ -118,6 +139,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
||||
[Description("Описание задачи")] string? description = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("CreateIssue", out var enabledError)) return enabledError;
|
||||
|
||||
if (!TryGetClient(out var client, out var error))
|
||||
{
|
||||
return error;
|
||||
@@ -190,6 +213,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
||||
[Description("Новое описание")] string? description = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("UpdateIssue", out var enabledError)) return enabledError;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(summary) && string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
return $"Нет полей для обновления задачи '{issueKey}'.";
|
||||
@@ -256,6 +281,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
||||
[Description("Ключ задачи")] string issueKey,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("GetIssueStatuses", out var enabledError)) return enabledError;
|
||||
|
||||
if (!TryGetClient(out var client, out var error))
|
||||
{
|
||||
return error;
|
||||
@@ -301,6 +328,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
||||
[Description("Максимум комментариев")] int limit = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("ListIssueComments", out var enabledError)) return enabledError;
|
||||
|
||||
if (!TryGetClient(out var client, out var error))
|
||||
{
|
||||
return error;
|
||||
@@ -349,6 +378,8 @@ public sealed class JiraIssueTools(JiraClientProvider provider, IConfiguration c
|
||||
[Description("Текст комментария")] string body,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("AddComment", out var enabledError)) return enabledError;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
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 k8s;
|
||||
using k8s.Autorest;
|
||||
using LazyBear.MCP.Services.ToolRegistry;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace LazyBear.MCP.Services.Kubernetes;
|
||||
|
||||
[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;
|
||||
|
||||
@@ -15,6 +21,8 @@ public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfigurat
|
||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("ListConfigMaps", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
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,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("GetConfigMapData", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateResourceName(name, nameof(name));
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
@@ -83,6 +93,8 @@ public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfigurat
|
||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("ListSecrets", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
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,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("GetSecretKeys", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateResourceName(name, nameof(name));
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
|
||||
@@ -3,12 +3,18 @@ using System.Text.Json;
|
||||
using k8s;
|
||||
using k8s.Autorest;
|
||||
using k8s.Models;
|
||||
using LazyBear.MCP.Services.ToolRegistry;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace LazyBear.MCP.Services.Kubernetes;
|
||||
|
||||
[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 MaxReplicas = 100;
|
||||
@@ -18,6 +24,8 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("ListDeployments", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
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,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("ScaleDeployment", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateResourceName(name, nameof(name));
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
|
||||
@@ -95,6 +105,8 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("GetRolloutStatus", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateResourceName(name, nameof(name));
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
@@ -126,6 +138,8 @@ public sealed class K8sDeploymentTools(K8sClientProvider clientProvider, IConfig
|
||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("RestartDeployment", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateResourceName(name, nameof(name));
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
using System.ComponentModel;
|
||||
using k8s;
|
||||
using k8s.Autorest;
|
||||
using LazyBear.MCP.Services.ToolRegistry;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace LazyBear.MCP.Services.Kubernetes;
|
||||
|
||||
[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")]
|
||||
public async Task<string> ListServices(
|
||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("ListServices", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
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,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("GetServiceDetails", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateResourceName(name, nameof(name));
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
@@ -90,6 +100,8 @@ public sealed class K8sNetworkTools(K8sClientProvider clientProvider, IConfigura
|
||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("ListIngresses", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
if (!TryGetClient(out var client, out var clientError))
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
using System.ComponentModel;
|
||||
using k8s;
|
||||
using k8s.Autorest;
|
||||
using LazyBear.MCP.Services.ToolRegistry;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace LazyBear.MCP.Services.Kubernetes;
|
||||
|
||||
[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 MinTailLines = 10;
|
||||
@@ -16,6 +22,8 @@ public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguratio
|
||||
[Description("Namespace Kubernetes")] string? @namespace = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("ListPods", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
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,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("GetPodStatus", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateResourceName(name, nameof(name));
|
||||
ValidateNamespace(@namespace ?? _defaultNamespace, nameof(@namespace));
|
||||
var ns = ResolveNamespace(@namespace);
|
||||
@@ -90,6 +100,8 @@ public sealed class K8sPodsTools(K8sClientProvider clientProvider, IConfiguratio
|
||||
int? tailLines = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!TryCheckEnabled("GetPodLogs", out var enabledError)) return enabledError;
|
||||
|
||||
ValidateResourceName(name, nameof(name));
|
||||
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 k8s;
|
||||
using LazyBear.MCP.Services.ToolRegistry;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace LazyBear.MCP.Services.Kubernetes;
|
||||
|
||||
[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 string? _clientInitializationError = clientProvider.InitializationError;
|
||||
protected readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default";
|
||||
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 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}";
|
||||
}
|
||||
Reference in New Issue
Block a user