Skip to main content
Glama

ARC Config MCP Server

by tsviz
kubernetes-enhanced.tsβ€’26.6 kB
/** * Enhanced Kubernetes Service for ARC Operations * * Comprehensive Kubernetes service with full ARC management capabilities. * Based on proven patterns from the arc-config-repo implementation. */ import * as k8s from '@kubernetes/client-node'; import type { Logger } from 'winston'; import type { IKubernetesService } from '../types/kubernetes.js'; export interface ClusterInfo { version: string; currentContext: string; nodeCount: number; readyNodes: number; totalCpu?: number; totalMemory?: number; } export interface DeploymentInfo { name: string; namespace: string; replicas: number; readyReplicas: number; availableReplicas: number; labels: Record<string, string>; securityContext?: any; resources?: any; } export interface NamespaceInfo { name: string; labels: Record<string, string>; status: string; } export interface NetworkPolicyInfo { name: string; namespace: string; labels: Record<string, string>; } export class KubernetesEnhancedService implements IKubernetesService { private kc: k8s.KubeConfig; private k8sApi: k8s.CoreV1Api; private appsApi: k8s.AppsV1Api; private networkingApi: k8s.NetworkingV1Api; private customObjectsApi: k8s.CustomObjectsApi; private logger: Logger; constructor(logger: Logger) { this.logger = logger; this.kc = new k8s.KubeConfig(); try { this.kc.loadFromDefault(); } catch (error) { this.logger.error('Failed to load Kubernetes configuration', { error }); throw new Error('Kubernetes configuration not found or invalid'); } this.k8sApi = this.kc.makeApiClient(k8s.CoreV1Api); this.appsApi = this.kc.makeApiClient(k8s.AppsV1Api); this.networkingApi = this.kc.makeApiClient(k8s.NetworkingV1Api); this.customObjectsApi = this.kc.makeApiClient(k8s.CustomObjectsApi); } /** * Get comprehensive cluster information */ async getClusterInfo(): Promise<ClusterInfo> { try { // Get version info from version API const versionApi = this.kc.makeApiClient(k8s.VersionApi); const versionResponse = await versionApi.getCode(); const nodesResponse = await this.k8sApi.listNode(); const nodes = (nodesResponse as any).body?.items || (nodesResponse as any).items || []; const readyNodes = nodes.filter((node: any) => node.status?.conditions?.some((condition: any) => condition.type === 'Ready' && condition.status === 'True' ) ).length; // Calculate total resources let totalCpu = 0; let totalMemory = 0; for (const node of nodes) { if (node.status?.capacity) { const cpu = node.status.capacity.cpu; const memory = node.status.capacity.memory; if (cpu) { // Convert CPU to millicores if (cpu.endsWith('m')) { totalCpu += parseInt(cpu.slice(0, -1)); } else { totalCpu += parseInt(cpu) * 1000; } } if (memory) { // Convert memory to Ki if (memory.endsWith('Ki')) { totalMemory += parseInt(memory.slice(0, -2)); } else if (memory.endsWith('Mi')) { totalMemory += parseInt(memory.slice(0, -2)) * 1024; } else if (memory.endsWith('Gi')) { totalMemory += parseInt(memory.slice(0, -2)) * 1024 * 1024; } } } } return { version: (versionResponse as any).body?.gitVersion || (versionResponse as any).gitVersion || 'unknown', currentContext: this.kc.getCurrentContext(), nodeCount: nodes.length, readyNodes, totalCpu, totalMemory }; } catch (error) { this.logger.error('Failed to get cluster info', { error }); throw new Error(`Failed to get cluster information: ${error}`); } } /** * Get API versions available in the cluster */ async getApiVersions(): Promise<string[]> { try { const apisApi = this.kc.makeApiClient(k8s.ApisApi); const response = await apisApi.getAPIVersions(); const groups = (response as any).body?.groups || (response as any).groups || []; return groups.map((g: any) => g.preferredVersion?.groupVersion || ''); } catch (error) { this.logger.error('Failed to get API versions', { error }); return []; } } /** * List all namespaces */ async listNamespaces(): Promise<NamespaceInfo[]> { try { const response = await this.k8sApi.listNamespace(); const items = (response as any).body?.items || (response as any).items || []; return items.map((ns: any) => ({ name: ns.metadata?.name || '', labels: ns.metadata?.labels || {}, status: ns.status?.phase || 'Unknown' })); } catch (error) { this.logger.error('Failed to list namespaces', { error }); throw new Error(`Failed to list namespaces: ${error}`); } } /** * Get a specific namespace */ async getNamespace(name: string): Promise<NamespaceInfo> { try { const response = await this.k8sApi.readNamespace(name); const ns = response.body; return { name: ns.metadata?.name || '', labels: ns.metadata?.labels || {}, status: ns.status?.phase || 'Unknown' }; } catch (error) { this.logger.error('Failed to get namespace', { namespace: name, error }); throw new Error(`Failed to get namespace ${name}: ${error}`); } } /** * Create a namespace with labels */ async createNamespace(name: string, labels: Record<string, string> = {}): Promise<void> { try { const namespace = { apiVersion: 'v1', kind: 'Namespace', metadata: { name, labels } }; await this.k8sApi.createNamespace(namespace); this.logger.info('Namespace created successfully', { namespace: name, labels }); } catch (error: any) { if (error.statusCode === 409) { // Namespace already exists, update labels this.logger.info('Namespace already exists, updating labels', { namespace: name }); await this.updateNamespaceLabels(name, labels); } else { this.logger.error('Failed to create namespace', { namespace: name, error }); throw new Error(`Failed to create namespace ${name}: ${error}`); } } } /** * Update namespace labels */ async updateNamespaceLabels(name: string, labels: Record<string, string>): Promise<void> { try { const patch = { metadata: { labels } }; const options = { headers: { 'Content-Type': 'application/merge-patch+json' } }; await this.k8sApi.patchNamespace( name, patch, undefined, undefined, undefined, undefined, undefined, options ); this.logger.info('Namespace labels updated', { namespace: name, labels }); } catch (error) { this.logger.error('Failed to update namespace labels', { namespace: name, error }); throw new Error(`Failed to update namespace labels for ${name}: ${error}`); } } /** * Delete a namespace */ async deleteNamespace(name: string): Promise<void> { try { await this.k8sApi.deleteNamespace(name); this.logger.info('Namespace deleted', { namespace: name }); } catch (error) { this.logger.error('Failed to delete namespace', { namespace: name, error }); throw new Error(`Failed to delete namespace ${name}: ${error}`); } } /** * List deployments in a namespace */ async listDeployments(namespace?: string): Promise<DeploymentInfo[]> { try { const response = namespace ? await this.appsApi.listNamespacedDeployment(namespace) : await this.appsApi.listDeploymentForAllNamespaces(); return response.body.items.map(deployment => ({ name: deployment.metadata?.name || '', namespace: deployment.metadata?.namespace || '', replicas: deployment.spec?.replicas || 0, readyReplicas: deployment.status?.readyReplicas || 0, availableReplicas: deployment.status?.availableReplicas || 0, labels: deployment.metadata?.labels || {}, securityContext: deployment.spec?.template?.spec?.securityContext, resources: deployment.spec?.template?.spec?.containers?.[0]?.resources })); } catch (error) { this.logger.error('Failed to list deployments', { namespace, error }); throw new Error(`Failed to list deployments: ${error}`); } } /** * Get deployment status */ async getDeploymentStatus(name: string, namespace: string): Promise<DeploymentInfo> { try { const response = await this.appsApi.readNamespacedDeployment(name, namespace); const deployment = response.body; return { name: deployment.metadata?.name || '', namespace: deployment.metadata?.namespace || '', replicas: deployment.spec?.replicas || 0, readyReplicas: deployment.status?.readyReplicas || 0, availableReplicas: deployment.status?.availableReplicas || 0, labels: deployment.metadata?.labels || {}, securityContext: deployment.spec?.template?.spec?.securityContext, resources: deployment.spec?.template?.spec?.containers?.[0]?.resources }; } catch (error) { this.logger.error('Failed to get deployment status', { name, namespace, error }); throw new Error(`Failed to get deployment ${name} in namespace ${namespace}: ${error}`); } } /** * Wait for a deployment to be ready */ async waitForDeployment(name: string, namespace: string, timeoutSeconds: number = 300): Promise<void> { const start = Date.now(); const timeout = timeoutSeconds * 1000; this.logger.info('Waiting for deployment to be ready', { name, namespace, timeoutSeconds }); while (Date.now() - start < timeout) { try { const deployment = await this.getDeploymentStatus(name, namespace); if (deployment.readyReplicas === deployment.replicas && deployment.replicas > 0) { this.logger.info('Deployment is ready', { name, namespace }); return; } // Wait 5 seconds before checking again await new Promise(resolve => setTimeout(resolve, 5000)); } catch (error) { // Deployment might not exist yet, continue waiting await new Promise(resolve => setTimeout(resolve, 5000)); } } throw new Error(`Timeout waiting for deployment ${name} in namespace ${namespace} to be ready`); } /** * Create a secret */ async createSecret( namespace: string, name: string, data: Record<string, string>, labels: Record<string, string> = {} ): Promise<void> { try { const secret = { apiVersion: 'v1', kind: 'Secret', metadata: { name, namespace, labels }, type: 'Opaque', stringData: data }; await this.k8sApi.createNamespacedSecret(namespace, secret); this.logger.info('Secret created successfully', { name, namespace }); } catch (error: any) { if (error.statusCode === 409) { // Secret already exists, update it await this.updateSecret(namespace, name, data, labels); } else { this.logger.error('Failed to create secret', { name, namespace, error }); throw new Error(`Failed to create secret ${name} in namespace ${namespace}: ${error}`); } } } /** * Update a secret */ async updateSecret( namespace: string, name: string, data: Record<string, string>, labels: Record<string, string> = {} ): Promise<void> { try { const secret = { apiVersion: 'v1', kind: 'Secret', metadata: { name, namespace, labels }, type: 'Opaque', stringData: data }; await this.k8sApi.replaceNamespacedSecret(name, namespace, secret); this.logger.info('Secret updated successfully', { name, namespace }); } catch (error) { this.logger.error('Failed to update secret', { name, namespace, error }); throw new Error(`Failed to update secret ${name} in namespace ${namespace}: ${error}`); } } /** * Get pod logs */ async getPodLogs(namespace: string, labelSelector: string, lines: number = 100): Promise<string> { try { // First, get pods with the label selector const podsResponse = await this.k8sApi.listNamespacedPod( namespace, undefined, undefined, undefined, undefined, labelSelector ); if (podsResponse.body.items.length === 0) { return 'No pods found with the specified label selector'; } // Get logs from the first pod const podName = podsResponse.body.items[0].metadata?.name; if (!podName) { return 'Pod name not found'; } const logsResponse = await this.k8sApi.readNamespacedPodLog( podName, namespace, undefined, // container undefined, // follow undefined, // insecureSkipTLSVerifyBackend undefined, // limitBytes undefined, // pretty undefined, // previous undefined, // sinceSeconds lines, // tailLines undefined // timestamps ); return logsResponse.body; } catch (error) { this.logger.error('Failed to get pod logs', { namespace, labelSelector, error }); throw new Error(`Failed to get pod logs: ${error}`); } } /** * List network policies */ async listNetworkPolicies(namespace: string): Promise<NetworkPolicyInfo[]> { try { const response = await this.networkingApi.listNamespacedNetworkPolicy(namespace); return response.body.items.map(policy => ({ name: policy.metadata?.name || '', namespace: policy.metadata?.namespace || '', labels: policy.metadata?.labels || {} })); } catch (error) { this.logger.error('Failed to list network policies', { namespace, error }); return []; } } /** * Apply a Kubernetes resource */ async applyResource(resource: any): Promise<void> { try { const { kind, metadata } = resource; const namespace = metadata?.namespace; const name = metadata?.name; switch (kind) { case 'NetworkPolicy': try { await this.networkingApi.createNamespacedNetworkPolicy(namespace, resource); } catch (error: any) { if (error.statusCode === 409) { await this.networkingApi.replaceNamespacedNetworkPolicy(name, namespace, resource); } else { throw error; } } break; case 'Secret': try { await this.k8sApi.createNamespacedSecret(namespace, resource); } catch (error: any) { if (error.statusCode === 409) { await this.k8sApi.replaceNamespacedSecret(name, namespace, resource); } else { throw error; } } break; case 'ConfigMap': try { await this.k8sApi.createNamespacedConfigMap(namespace, resource); } catch (error: any) { if (error.statusCode === 409) { await this.k8sApi.replaceNamespacedConfigMap(name, namespace, resource); } else { throw error; } } break; default: // For custom resources, use the custom objects API const parts = resource.apiVersion.split('/'); const group = parts.length === 2 ? parts[0] : ''; const version = parts.length === 2 ? parts[1] : parts[0]; const plural = `${kind.toLowerCase()}s`; // Simple pluralization try { await this.customObjectsApi.createNamespacedCustomObject( group, version, namespace, plural, resource ); } catch (error: any) { if (error.statusCode === 409) { await this.customObjectsApi.replaceNamespacedCustomObject( group, version, namespace, plural, name, resource ); } else { throw error; } } break; } this.logger.info('Resource applied successfully', { kind, name, namespace }); } catch (error) { this.logger.error('Failed to apply resource', { resource: resource.kind, error }); throw new Error(`Failed to apply ${resource.kind}: ${error}`); } } /** * Get custom resources */ async getCustomResources(apiVersion: string, kind: string, namespace?: string): Promise<any[]> { try { const parts = apiVersion.split('/'); const group = parts.length === 2 ? parts[0] : ''; const version = parts.length === 2 ? parts[1] : parts[0]; const plural = `${kind.toLowerCase()}s`; // Simple pluralization const response = namespace ? await this.customObjectsApi.listNamespacedCustomObject(group, version, namespace, plural) : await this.customObjectsApi.listClusterCustomObject(group, version, plural); return (response.body as any).items || []; } catch (error) { this.logger.error('Failed to get custom resources', { apiVersion, kind, namespace, error }); return []; } } /** * Delete custom resources */ async deleteCustomResources(apiVersion: string, kind: string, namespace: string): Promise<void> { try { const resources = await this.getCustomResources(apiVersion, kind, namespace); const parts = apiVersion.split('/'); const group = parts.length === 2 ? parts[0] : ''; const version = parts.length === 2 ? parts[1] : parts[0]; const plural = `${kind.toLowerCase()}s`; for (const resource of resources) { const name = resource.metadata?.name; if (name) { await this.customObjectsApi.deleteNamespacedCustomObject( group, version, namespace, plural, name ); } } this.logger.info('Custom resources deleted', { apiVersion, kind, namespace, count: resources.length }); } catch (error) { this.logger.error('Failed to delete custom resources', { apiVersion, kind, namespace, error }); throw new Error(`Failed to delete custom resources: ${error}`); } } /** * Delete Helm release (requires helm CLI) */ async deleteHelmRelease(releaseName: string, namespace: string): Promise<void> { const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); try { await execAsync(`helm uninstall ${releaseName} --namespace ${namespace}`); this.logger.info('Helm release deleted', { releaseName, namespace }); } catch (error) { this.logger.error('Failed to delete Helm release', { releaseName, namespace, error }); throw new Error(`Failed to delete Helm release ${releaseName}: ${error}`); } } /** * Apply Kubernetes manifest (compatibility method) */ async applyManifest(manifest: any): Promise<any> { await this.applyResource(manifest); return { applied: true, resource: manifest }; } /** * Delete a Kubernetes resource (compatibility method) */ async deleteResource(namespace: string, name: string, resourceType: string): Promise<void> { this.logger.info(`Deleting ${resourceType} ${name} in namespace ${namespace}`); try { if (resourceType.toLowerCase() === 'namespace') { await this.deleteNamespace(name); } else if (resourceType.toLowerCase() === 'deployment') { await this.appsApi.deleteNamespacedDeployment(name, namespace); } else if (resourceType.toLowerCase() === 'secret') { await this.k8sApi.deleteNamespacedSecret(name, namespace); } else if (resourceType.toLowerCase() === 'configmap') { await this.k8sApi.deleteNamespacedConfigMap(name, namespace); } else { // Try as custom resource const parts = resourceType.split('/'); if (parts.length === 2) { const [group, version] = parts; const plural = `${resourceType.toLowerCase()}s`; await this.customObjectsApi.deleteNamespacedCustomObject( group, version, namespace, plural, name ); } } } catch (error) { this.logger.error(`Failed to delete ${resourceType} ${name}`, { error }); throw error; } } /** * Scale deployment (compatibility method) */ async scaleDeployment(namespace: string, name: string, replicas: number): Promise<void> { try { const patch = { spec: { replicas: replicas } }; await this.appsApi.patchNamespacedDeployment( name, namespace, patch, undefined, undefined, undefined, undefined, undefined, { headers: { 'Content-Type': 'application/merge-patch+json' } } ); this.logger.info(`Scaled deployment ${name} to ${replicas} replicas`, { namespace }); } catch (error) { this.logger.error(`Failed to scale deployment ${name}`, { namespace, replicas, error }); throw new Error(`Failed to scale deployment ${name}: ${error}`); } } /** * Execute kubectl command (compatibility method) */ async execute(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }> { const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); try { const fullCommand = command.startsWith('kubectl') ? command : `kubectl ${command}`; const { stdout, stderr } = await execAsync(fullCommand, { maxBuffer: 1024 * 1024 * 10, // 10MB buffer timeout: 30000 // 30 second timeout }); this.logger.debug('Kubectl command executed', { command: fullCommand, stdout: stdout.slice(0, 500) }); return { stdout: stdout || '', stderr: stderr || '', exitCode: 0 }; } catch (error: any) { this.logger.error('Kubectl command failed', { command, error: error.message }); return { stdout: error.stdout || '', stderr: error.stderr || error.message || '', exitCode: error.code || 1 }; } } /** * Get pods in namespace for compatibility */ async getPodsInNamespace(namespace: string): Promise<any[]> { try { const response = await this.k8sApi.listNamespacedPod(namespace); return response.body.items; } catch (error) { this.logger.error('Failed to get pods in namespace', { namespace, error }); return []; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tsviz/arc-config-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server