feat: add index.js for mcp-kubernetes server
This commit is contained in:
394
apps/ollama-mcp/mcp-kubernetes/index.js
Normal file
394
apps/ollama-mcp/mcp-kubernetes/index.js
Normal file
@@ -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`);
|
||||
});
|
||||
Reference in New Issue
Block a user