using System.ComponentModel; using k8s; using k8s.Autorest; using ModelContextProtocol.Server; namespace LazyBear.MCP.Services.Kubernetes; [McpServerToolType] public sealed class K8sNetworkTools(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("Список service в namespace")] public async Task ListServices( [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 services = await client.CoreV1.ListNamespacedServiceAsync(ns, cancellationToken: cancellationToken); if (services.Items.Count == 0) { return $"В namespace '{ns}' service не найдены."; } var lines = services.Items.Select(svc => { var name = svc.Metadata?.Name ?? "unknown"; var type = svc.Spec?.Type ?? "ClusterIP"; var clusterIp = svc.Spec?.ClusterIP ?? "-"; var ports = svc.Spec?.Ports is null ? "-" : string.Join(",", svc.Spec.Ports.Select(p => $"{p.Port}/{p.Protocol}")); return $"{name}: type={type}, clusterIp={clusterIp}, ports={ports}"; }); return $"Services namespace '{ns}':\n{string.Join('\n', lines)}"; } catch (Exception ex) { return FormatError("list_services", ns, ex); } } [McpServerTool, Description("Детали service")] public async Task GetServiceDetails( [Description("Имя service")] 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 service = await client.CoreV1.ReadNamespacedServiceAsync(name, ns, cancellationToken: cancellationToken); var type = service.Spec?.Type ?? "ClusterIP"; var clusterIp = service.Spec?.ClusterIP ?? "-"; var externalIps = service.Spec?.ExternalIPs is null ? "-" : string.Join(",", service.Spec.ExternalIPs); var selector = service.Spec?.Selector is null ? "-" : string.Join(",", service.Spec.Selector.Select(x => $"{x.Key}={x.Value}")); var ports = service.Spec?.Ports is null ? "-" : string.Join(",", service.Spec.Ports.Select(p => $"{p.Name ?? "port"}:{p.Port}->{p.TargetPort}")); return $"Service '{name}' namespace '{ns}': type={type}, clusterIp={clusterIp}, externalIps={externalIps}, selector={selector}, ports={ports}"; } catch (Exception ex) { return FormatError("get_service_details", ns, ex, name); } } [McpServerTool, Description("Список ingress в namespace")] public async Task ListIngresses( [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 ingresses = await client.NetworkingV1.ListNamespacedIngressAsync(ns, cancellationToken: cancellationToken); if (ingresses.Items.Count == 0) { return $"В namespace '{ns}' ingress не найдены."; } var lines = ingresses.Items.Select(ing => { var name = ing.Metadata?.Name ?? "unknown"; var rules = ing.Spec?.Rules is null ? "-" : string.Join(";", ing.Spec.Rules.Select(r => { var host = string.IsNullOrWhiteSpace(r.Host) ? "*" : r.Host; var paths = r.Http?.Paths is null ? "-" : string.Join(",", r.Http.Paths.Select(p => { var serviceName = p.Backend?.Service?.Name ?? "-"; var servicePort = p.Backend?.Service?.Port?.Number?.ToString() ?? p.Backend?.Service?.Port?.Name ?? "-"; return $"{p.Path ?? "/"}->{serviceName}:{servicePort}"; })); return $"{host}:{paths}"; })); return $"{name}: rules={rules}"; }); return $"Ingress namespace '{ns}':\n{string.Join('\n', lines)}"; } catch (Exception ex) { return FormatError("list_ingresses", ns, ex); } } 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}"; } }