using System.ComponentModel; using k8s; using k8s.Autorest; using ModelContextProtocol.Server; namespace LazyBear.MCP.Services.Kubernetes; [McpServerToolType] public sealed class K8sConfigTools(K8sClientProvider clientProvider, IConfiguration configuration) { private readonly IKubernetes? _client = clientProvider.Client; private readonly string? _clientInitializationError = clientProvider.InitializationError; private readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default"; [McpServerTool, Description("Список ConfigMap в namespace")] public async Task ListConfigMaps( [Description("Namespace Kubernetes")] string? @namespace = null, CancellationToken cancellationToken = default) { var ns = ResolveNamespace(@namespace); if (!TryGetClient(out var client, out var clientError)) { return clientError; } try { var configMaps = await client.CoreV1.ListNamespacedConfigMapAsync(ns, cancellationToken: cancellationToken); if (configMaps.Items.Count == 0) { return $"В namespace '{ns}' configMap не найдены."; } var lines = configMaps.Items.Select(cm => { var name = cm.Metadata?.Name ?? "unknown"; var keyCount = cm.Data?.Count ?? 0; return $"{name}: keys={keyCount}"; }); return $"ConfigMap namespace '{ns}':\n{string.Join('\n', lines)}"; } catch (Exception ex) { return FormatError("list_config_maps", ns, ex); } } [McpServerTool, Description("Получить данные ConfigMap")] public async Task GetConfigMapData( [Description("Имя ConfigMap")] string name, [Description("Namespace Kubernetes")] string? @namespace = null, CancellationToken cancellationToken = default) { var ns = ResolveNamespace(@namespace); if (!TryGetClient(out var client, out var clientError)) { return clientError; } try { var configMap = await client.CoreV1.ReadNamespacedConfigMapAsync(name, ns, cancellationToken: cancellationToken); if (configMap.Data is null || configMap.Data.Count == 0) { return $"ConfigMap '{name}' в namespace '{ns}' не содержит данных."; } var lines = configMap.Data.Select(item => $"{item.Key}={item.Value}"); return $"Данные ConfigMap '{name}' namespace '{ns}':\n{string.Join('\n', lines)}"; } catch (Exception ex) { return FormatError("get_config_map_data", ns, ex, name); } } [McpServerTool, Description("Список Secret в namespace (без значений)")] public async Task ListSecrets( [Description("Namespace Kubernetes")] string? @namespace = null, CancellationToken cancellationToken = default) { var ns = ResolveNamespace(@namespace); if (!TryGetClient(out var client, out var clientError)) { return clientError; } try { var secrets = await client.CoreV1.ListNamespacedSecretAsync(ns, cancellationToken: cancellationToken); if (secrets.Items.Count == 0) { return $"В namespace '{ns}' secret не найдены."; } var lines = secrets.Items.Select(secret => { var name = secret.Metadata?.Name ?? "unknown"; var type = secret.Type ?? "Opaque"; return $"{name}: type={type}"; }); return $"Secret namespace '{ns}':\n{string.Join('\n', lines)}"; } catch (Exception ex) { return FormatError("list_secrets", ns, ex); } } [McpServerTool, Description("Ключи Secret без значений")] public async Task GetSecretKeys( [Description("Имя Secret")] string name, [Description("Namespace Kubernetes")] string? @namespace = null, CancellationToken cancellationToken = default) { var ns = ResolveNamespace(@namespace); if (!TryGetClient(out var client, out var clientError)) { return clientError; } try { var secret = await client.CoreV1.ReadNamespacedSecretAsync(name, ns, cancellationToken: cancellationToken); var keys = new HashSet(StringComparer.Ordinal); if (secret.Data is not null) { foreach (var key in secret.Data.Keys) { keys.Add(key); } } if (secret.StringData is not null) { foreach (var key in secret.StringData.Keys) { keys.Add(key); } } if (keys.Count == 0) { return $"Secret '{name}' в namespace '{ns}' не содержит ключей."; } return $"Ключи Secret '{name}' namespace '{ns}': {string.Join(", ", keys.OrderBy(k => k))}"; } catch (Exception ex) { return FormatError("get_secret_keys", ns, ex, name); } } private string ResolveNamespace(string? @namespace) { return string.IsNullOrWhiteSpace(@namespace) ? _defaultNamespace : @namespace; } private static string BuildClientInitializationError() { return "Kubernetes клиент не инициализирован. Проверьте Kubernetes:KubeconfigPath или in-cluster окружение (KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT)."; } private bool TryGetClient(out IKubernetes client, out string error) { if (_client is null) { client = null!; var details = string.IsNullOrWhiteSpace(_clientInitializationError) ? string.Empty : $" Детали: {_clientInitializationError}"; error = BuildClientInitializationError() + details; return false; } client = _client; error = string.Empty; return true; } private static string FormatError(string toolName, string @namespace, Exception exception, string? resourceName = null) { var resourcePart = string.IsNullOrWhiteSpace(resourceName) ? string.Empty : $", resource='{resourceName}'"; if (exception is HttpOperationException httpException) { var statusCode = httpException.Response?.StatusCode; var reason = httpException.Response?.ReasonPhrase; var body = string.IsNullOrWhiteSpace(httpException.Response?.Content) ? "-" : httpException.Response!.Content; return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): status={(int?)statusCode ?? 0} {reason}, details={body}"; } return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): {exception.GetType().Name}: {exception.Message}"; } }