const express = require('express'); const k8s = require('@kubernetes/client-node'); const app = express(); const port = process.env.PORT || 3000; // Kubernetes client setup const kc = new k8s.KubeConfig(); kc.loadFromDefault(); const k8sApi = kc.makeApiClient(k8s.CoreV1Api); const k8sAppsApi = kc.makeApiClient(k8s.AppsV1Api); const k8sBatchApi = kc.makeApiClient(k8s.BatchV1Api); app.use(express.json()); // CORS middleware app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); // Health check app.get('/health', (req, res) => { res.json({ status: 'healthy', service: 'mcp-kubernetes', timestamp: new Date().toISOString() }); }); // Root endpoint app.get('/', (req, res) => { res.json({ service: 'MCP Kubernetes Server', version: '1.0.0', endpoints: { health: 'GET /health', pods: { list: 'POST /api/pods/list', get: 'POST /api/pods/get', logs: 'POST /api/pods/logs', delete: 'POST /api/pods/delete' }, deployments: { list: 'POST /api/deployments/list', get: 'POST /api/deployments/get', scale: 'POST /api/deployments/scale' }, services: { list: 'POST /api/services/list', get: 'POST /api/services/get' }, namespaces: { list: 'POST /api/namespaces/list' }, nodes: { list: 'POST /api/nodes/list' } } }); }); // ============================================================================ // PODS // ============================================================================ // List pods app.post('/api/pods/list', async (req, res) => { try { const namespace = req.body.namespace || 'default'; const labelSelector = req.body.labelSelector || ''; const response = await k8sApi.listNamespacedPod( namespace, undefined, undefined, undefined, undefined, labelSelector ); const pods = response.body.items.map(pod => ({ name: pod.metadata.name, namespace: pod.metadata.namespace, status: pod.status.phase, ready: pod.status.containerStatuses ? `${pod.status.containerStatuses.filter(c => c.ready).length}/${pod.status.containerStatuses.length}` : '0/0', restarts: pod.status.containerStatuses ? pod.status.containerStatuses.reduce((sum, c) => sum + c.restartCount, 0) : 0, age: pod.metadata.creationTimestamp, node: pod.spec.nodeName, ip: pod.status.podIP })); res.json({ count: pods.length, pods }); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // Get pod details app.post('/api/pods/get', async (req, res) => { try { const { name, namespace } = req.body; if (!name) { return res.status(400).json({ error: 'Pod name is required' }); } const response = await k8sApi.readNamespacedPod( name, namespace || 'default' ); res.json(response.body); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // Get pod logs app.post('/api/pods/logs', async (req, res) => { try { const { name, namespace, container, tailLines, since, timestamps } = req.body; if (!name) { return res.status(400).json({ error: 'Pod name is required' }); } const response = await k8sApi.readNamespacedPodLog( name, namespace || 'default', container, undefined, undefined, undefined, undefined, undefined, since, tailLines || 100, timestamps || false ); res.json({ pod: name, namespace: namespace || 'default', container: container || 'default', logs: response.body }); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // Delete pod app.post('/api/pods/delete', async (req, res) => { try { const { name, namespace } = req.body; if (!name) { return res.status(400).json({ error: 'Pod name is required' }); } const response = await k8sApi.deleteNamespacedPod( name, namespace || 'default' ); res.json({ message: 'Pod deleted successfully', pod: name, namespace: namespace || 'default' }); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // ============================================================================ // DEPLOYMENTS // ============================================================================ // List deployments app.post('/api/deployments/list', async (req, res) => { try { const namespace = req.body.namespace || 'default'; const response = await k8sAppsApi.listNamespacedDeployment(namespace); const deployments = response.body.items.map(dep => ({ name: dep.metadata.name, namespace: dep.metadata.namespace, replicas: `${dep.status.readyReplicas || 0}/${dep.spec.replicas}`, available: dep.status.availableReplicas || 0, age: dep.metadata.creationTimestamp, image: dep.spec.template.spec.containers[0].image })); res.json({ count: deployments.length, deployments }); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // Get deployment app.post('/api/deployments/get', async (req, res) => { try { const { name, namespace } = req.body; if (!name) { return res.status(400).json({ error: 'Deployment name is required' }); } const response = await k8sAppsApi.readNamespacedDeployment( name, namespace || 'default' ); res.json(response.body); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // Scale deployment app.post('/api/deployments/scale', async (req, res) => { try { const { name, namespace, replicas } = req.body; if (!name) { return res.status(400).json({ error: 'Deployment name is required' }); } if (replicas === undefined) { return res.status(400).json({ error: 'Replicas count is required' }); } const patch = { spec: { replicas: parseInt(replicas) } }; const options = { headers: { 'Content-Type': 'application/strategic-merge-patch+json' } }; const response = await k8sAppsApi.patchNamespacedDeployment( name, namespace || 'default', patch, undefined, undefined, undefined, undefined, options ); res.json({ message: 'Deployment scaled successfully', deployment: name, namespace: namespace || 'default', replicas: replicas }); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // ============================================================================ // SERVICES // ============================================================================ // List services app.post('/api/services/list', async (req, res) => { try { const namespace = req.body.namespace || 'default'; const response = await k8sApi.listNamespacedService(namespace); const services = response.body.items.map(svc => ({ name: svc.metadata.name, namespace: svc.metadata.namespace, type: svc.spec.type, clusterIP: svc.spec.clusterIP, externalIP: svc.status.loadBalancer?.ingress?.[0]?.ip || 'none', ports: svc.spec.ports?.map(p => `${p.port}:${p.targetPort}/${p.protocol}`).join(', '), age: svc.metadata.creationTimestamp })); res.json({ count: services.length, services }); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // Get service app.post('/api/services/get', async (req, res) => { try { const { name, namespace } = req.body; if (!name) { return res.status(400).json({ error: 'Service name is required' }); } const response = await k8sApi.readNamespacedService( name, namespace || 'default' ); res.json(response.body); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // ============================================================================ // NAMESPACES // ============================================================================ // List namespaces app.post('/api/namespaces/list', async (req, res) => { try { const response = await k8sApi.listNamespace(); const namespaces = response.body.items.map(ns => ({ name: ns.metadata.name, status: ns.status.phase, age: ns.metadata.creationTimestamp })); res.json({ count: namespaces.length, namespaces }); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // ============================================================================ // NODES // ============================================================================ // List nodes app.post('/api/nodes/list', async (req, res) => { try { const response = await k8sApi.listNode(); const nodes = response.body.items.map(node => ({ name: node.metadata.name, status: node.status.conditions?.find(c => c.type === 'Ready')?.status === 'True' ? 'Ready' : 'NotReady', roles: node.metadata.labels?.['node-role.kubernetes.io/master'] ? 'master' : 'worker', age: node.metadata.creationTimestamp, version: node.status.nodeInfo.kubeletVersion, internalIP: node.status.addresses?.find(a => a.type === 'InternalIP')?.address, os: `${node.status.nodeInfo.osImage}`, kernel: node.status.nodeInfo.kernelVersion, containerRuntime: node.status.nodeInfo.containerRuntimeVersion })); res.json({ count: nodes.length, nodes }); } catch (error) { res.status(500).json({ error: error.message, details: error.body }); } }); // ============================================================================ // ERROR HANDLING // ============================================================================ // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found', message: `${req.method} ${req.path} is not a valid endpoint`, availableEndpoints: 'GET / for list of available endpoints' }); }); // Error handler app.use((err, req, res, next) => { console.error('Error:', err); res.status(500).json({ error: 'Internal server error', message: err.message }); }); // ============================================================================ // START SERVER // ============================================================================ app.listen(port, '0.0.0.0', () => { console.log(`╔═══════════════════════════════════════════════════════╗`); console.log(`║ ║`); console.log(`║ MCP Kubernetes Server ║`); console.log(`║ Running on port ${port} ║`); console.log(`║ ║`); console.log(`╚═══════════════════════════════════════════════════════╝`); console.log(`\nEndpoints available at http://0.0.0.0:${port}`); console.log(`Health check: http://0.0.0.0:${port}/health`); });