using System.ComponentModel; using k8s; using k8s.Autorest; using ModelContextProtocol.Server; namespace LazyBear.MCP.Services.Kubernetes; [McpServerToolType] public sealed class K8sPodsTools(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("Список подов в namespace")] public async Task ListPods( [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 pods = await client.CoreV1.ListNamespacedPodAsync(ns, cancellationToken: cancellationToken); if (pods.Items.Count == 0) { return $"В namespace '{ns}' поды не найдены."; } var lines = pods.Items.Select(pod => { var podName = pod.Metadata?.Name ?? "unknown"; var phase = pod.Status?.Phase ?? "Unknown"; var node = pod.Spec?.NodeName ?? "-"; var restarts = pod.Status?.ContainerStatuses?.Sum(s => s.RestartCount) ?? 0; return $"{podName}: phase={phase}, restarts={restarts}, node={node}"; }); return $"Поды namespace '{ns}':\n{string.Join('\n', lines)}"; } catch (Exception ex) { return FormatError("list_pods", ns, ex); } } [McpServerTool, Description("Статус pod по имени")] public async Task GetPodStatus( [Description("Имя pod")] 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 pod = await client.CoreV1.ReadNamespacedPodAsync(name, ns, cancellationToken: cancellationToken); var phase = pod.Status?.Phase ?? "Unknown"; var hostIp = pod.Status?.HostIP ?? "-"; var podIp = pod.Status?.PodIP ?? "-"; var conditions = pod.Status?.Conditions?.Select(c => $"{c.Type}={c.Status}") ?? []; return $"Pod '{name}' в namespace '{ns}': phase={phase}, hostIp={hostIp}, podIp={podIp}, conditions=[{string.Join(", ", conditions)}]"; } catch (Exception ex) { return FormatError("get_pod_status", ns, ex, name); } } [McpServerTool, Description("Логи pod")] public async Task GetPodLogs( [Description("Имя pod")] string name, [Description("Namespace Kubernetes")] string? @namespace = null, [Description("Имя контейнера")] string? container = null, [Description("Количество строк с конца")] int? tailLines = 100, CancellationToken cancellationToken = default) { var ns = ResolveNamespace(@namespace); if (!TryGetClient(out var client, out var clientError)) { return clientError; } try { await using var logStream = await client.CoreV1.ReadNamespacedPodLogAsync( name, ns, container: container, tailLines: tailLines, cancellationToken: cancellationToken); using var reader = new StreamReader(logStream); var logs = await reader.ReadToEndAsync(cancellationToken); if (string.IsNullOrWhiteSpace(logs)) { return $"Логи pod '{name}' в namespace '{ns}' пустые."; } return logs; } catch (Exception ex) { return FormatError("get_pod_logs", 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}"; } }