Files
k3s-gitops/apps/ollama-mcp/mcp-kubernetes/index.js
2026-01-11 13:18:27 +00:00

395 lines
12 KiB
JavaScript

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`);
});