From cd713b32ed2a2a283f12f0540d6705e22204c9fe Mon Sep 17 00:00:00 2001 From: Claude AI Date: Sun, 11 Jan 2026 13:18:27 +0000 Subject: [PATCH] feat: add index.js for mcp-kubernetes server --- apps/ollama-mcp/mcp-kubernetes/index.js | 394 ++++++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 apps/ollama-mcp/mcp-kubernetes/index.js diff --git a/apps/ollama-mcp/mcp-kubernetes/index.js b/apps/ollama-mcp/mcp-kubernetes/index.js new file mode 100644 index 0000000..e81b2f7 --- /dev/null +++ b/apps/ollama-mcp/mcp-kubernetes/index.js @@ -0,0 +1,394 @@ +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`); +});