Add Proxmox MCP server with configurable permissions
- Node.js implementation matching other MCP servers - Configurable permission levels via PROXMOX_ALLOW_ELEVATED - Basic mode: node listing, VM status, cluster overview - Elevated mode: detailed metrics, command execution - Environment variable loading from .env file
This commit is contained in:
562
index.js
Normal file
562
index.js
Normal file
@@ -0,0 +1,562 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import fetch from 'node-fetch';
|
||||
import https from 'https';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Load environment variables from .env file
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const envPath = join(__dirname, '../../.env');
|
||||
|
||||
try {
|
||||
const envFile = readFileSync(envPath, 'utf8');
|
||||
const envVars = envFile.split('\n').filter(line => line.includes('='));
|
||||
for (const line of envVars) {
|
||||
const [key, ...values] = line.split('=');
|
||||
if (key && values.length > 0) {
|
||||
process.env[key.trim()] = values.join('=').trim();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Warning: Could not load .env file:', error.message);
|
||||
}
|
||||
|
||||
class ProxmoxServer {
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'proxmox-server',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.proxmoxHost = process.env.PROXMOX_HOST || '192.168.6.247';
|
||||
this.proxmoxUser = process.env.PROXMOX_USER || 'root@pam';
|
||||
this.proxmoxTokenName = process.env.PROXMOX_TOKEN_NAME || 'mcpserver';
|
||||
this.proxmoxTokenValue = process.env.PROXMOX_TOKEN_VALUE;
|
||||
this.proxmoxPort = process.env.PROXMOX_PORT || '8006';
|
||||
this.allowElevated = process.env.PROXMOX_ALLOW_ELEVATED === 'true';
|
||||
|
||||
// Create agent that accepts self-signed certificates
|
||||
this.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
|
||||
this.setupToolHandlers();
|
||||
}
|
||||
|
||||
async proxmoxRequest(endpoint, method = 'GET', body = null) {
|
||||
const baseUrl = `https://${this.proxmoxHost}:${this.proxmoxPort}/api2/json`;
|
||||
const url = `${baseUrl}${endpoint}`;
|
||||
|
||||
const headers = {
|
||||
'Authorization': `PVEAPIToken=${this.proxmoxUser}!${this.proxmoxTokenName}=${this.proxmoxTokenValue}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
const options = {
|
||||
method,
|
||||
headers,
|
||||
agent: this.httpsAgent
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Proxmox API error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const textResponse = await response.text();
|
||||
if (!textResponse.trim()) {
|
||||
throw new Error('Empty response from Proxmox API');
|
||||
}
|
||||
|
||||
const data = JSON.parse(textResponse);
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
if (error.name === 'SyntaxError') {
|
||||
throw new Error(`Failed to parse Proxmox API response: ${error.message}`);
|
||||
}
|
||||
throw new Error(`Failed to connect to Proxmox: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
setupToolHandlers() {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: 'proxmox_get_nodes',
|
||||
description: 'List all Proxmox cluster nodes with their status and resources',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'proxmox_get_node_status',
|
||||
description: 'Get detailed status information for a specific Proxmox node',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
node: { type: 'string', description: 'Node name (e.g., pve1, proxmox-node2)' }
|
||||
},
|
||||
required: ['node']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'proxmox_get_vms',
|
||||
description: 'List all virtual machines across the cluster with their status',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
node: { type: 'string', description: 'Optional: filter by specific node' },
|
||||
type: { type: 'string', enum: ['qemu', 'lxc', 'all'], description: 'VM type filter', default: 'all' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'proxmox_get_vm_status',
|
||||
description: 'Get detailed status information for a specific VM',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
node: { type: 'string', description: 'Node name where VM is located' },
|
||||
vmid: { type: 'string', description: 'VM ID number' },
|
||||
type: { type: 'string', enum: ['qemu', 'lxc'], description: 'VM type', default: 'qemu' }
|
||||
},
|
||||
required: ['node', 'vmid']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'proxmox_execute_vm_command',
|
||||
description: 'Execute a shell command on a virtual machine via Proxmox API',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
node: { type: 'string', description: 'Node name where VM is located' },
|
||||
vmid: { type: 'string', description: 'VM ID number' },
|
||||
command: { type: 'string', description: 'Shell command to execute' },
|
||||
type: { type: 'string', enum: ['qemu', 'lxc'], description: 'VM type', default: 'qemu' }
|
||||
},
|
||||
required: ['node', 'vmid', 'command']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'proxmox_get_storage',
|
||||
description: 'List all storage pools and their usage across the cluster',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
node: { type: 'string', description: 'Optional: filter by specific node' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'proxmox_get_cluster_status',
|
||||
description: 'Get overall cluster status including nodes and resource usage',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
case 'proxmox_get_nodes':
|
||||
return await this.getNodes();
|
||||
|
||||
case 'proxmox_get_node_status':
|
||||
return await this.getNodeStatus(args.node);
|
||||
|
||||
case 'proxmox_get_vms':
|
||||
return await this.getVMs(args.node, args.type);
|
||||
|
||||
case 'proxmox_get_vm_status':
|
||||
return await this.getVMStatus(args.node, args.vmid, args.type);
|
||||
|
||||
case 'proxmox_execute_vm_command':
|
||||
return await this.executeVMCommand(args.node, args.vmid, args.command, args.type);
|
||||
|
||||
case 'proxmox_get_storage':
|
||||
return await this.getStorage(args.node);
|
||||
|
||||
case 'proxmox_get_cluster_status':
|
||||
return await this.getClusterStatus();
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error.message}`
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getNodes() {
|
||||
const nodes = await this.proxmoxRequest('/nodes');
|
||||
|
||||
let output = '🖥️ **Proxmox Cluster Nodes**\n\n';
|
||||
|
||||
for (const node of nodes) {
|
||||
const status = node.status === 'online' ? '🟢' : '🔴';
|
||||
const uptime = node.uptime ? this.formatUptime(node.uptime) : 'N/A';
|
||||
const cpuUsage = node.cpu ? `${(node.cpu * 100).toFixed(1)}%` : 'N/A';
|
||||
const memUsage = node.mem && node.maxmem ?
|
||||
`${this.formatBytes(node.mem)} / ${this.formatBytes(node.maxmem)} (${((node.mem / node.maxmem) * 100).toFixed(1)}%)` : 'N/A';
|
||||
|
||||
output += `${status} **${node.node}**\n`;
|
||||
output += ` • Status: ${node.status}\n`;
|
||||
output += ` • Uptime: ${uptime}\n`;
|
||||
output += ` • CPU: ${cpuUsage}\n`;
|
||||
output += ` • Memory: ${memUsage}\n`;
|
||||
output += ` • Load: ${node.loadavg?.[0]?.toFixed(2) || 'N/A'}\n\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }]
|
||||
};
|
||||
}
|
||||
|
||||
async getNodeStatus(node) {
|
||||
if (!this.allowElevated) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `⚠️ **Node Status Requires Elevated Permissions**\n\nTo view detailed node status, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file and ensure your API token has Sys.Audit permissions.\n\n**Current permissions**: Basic (node listing only)`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const status = await this.proxmoxRequest(`/nodes/${node}/status`);
|
||||
|
||||
let output = `🖥️ **Node ${node} Status**\n\n`;
|
||||
output += `• **Status**: ${status.uptime ? '🟢 Online' : '🔴 Offline'}\n`;
|
||||
output += `• **Uptime**: ${status.uptime ? this.formatUptime(status.uptime) : 'N/A'}\n`;
|
||||
output += `• **Load Average**: ${status.loadavg?.join(', ') || 'N/A'}\n`;
|
||||
output += `• **CPU Usage**: ${status.cpu ? `${(status.cpu * 100).toFixed(1)}%` : 'N/A'}\n`;
|
||||
output += `• **Memory**: ${status.memory ?
|
||||
`${this.formatBytes(status.memory.used)} / ${this.formatBytes(status.memory.total)} (${((status.memory.used / status.memory.total) * 100).toFixed(1)}%)` : 'N/A'}\n`;
|
||||
output += `• **Root Disk**: ${status.rootfs ?
|
||||
`${this.formatBytes(status.rootfs.used)} / ${this.formatBytes(status.rootfs.total)} (${((status.rootfs.used / status.rootfs.total) * 100).toFixed(1)}%)` : 'N/A'}\n`;
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }]
|
||||
};
|
||||
}
|
||||
|
||||
async getVMs(nodeFilter = null, typeFilter = 'all') {
|
||||
let vms = [];
|
||||
|
||||
if (nodeFilter) {
|
||||
const nodeVMs = await this.proxmoxRequest(`/nodes/${nodeFilter}/qemu`);
|
||||
const nodeLXCs = await this.proxmoxRequest(`/nodes/${nodeFilter}/lxc`);
|
||||
|
||||
if (typeFilter === 'all' || typeFilter === 'qemu') {
|
||||
vms.push(...nodeVMs.map(vm => ({ ...vm, type: 'qemu', node: nodeFilter })));
|
||||
}
|
||||
if (typeFilter === 'all' || typeFilter === 'lxc') {
|
||||
vms.push(...nodeLXCs.map(vm => ({ ...vm, type: 'lxc', node: nodeFilter })));
|
||||
}
|
||||
} else {
|
||||
const nodes = await this.proxmoxRequest('/nodes');
|
||||
|
||||
for (const node of nodes) {
|
||||
if (typeFilter === 'all' || typeFilter === 'qemu') {
|
||||
const nodeVMs = await this.proxmoxRequest(`/nodes/${node.node}/qemu`);
|
||||
vms.push(...nodeVMs.map(vm => ({ ...vm, type: 'qemu', node: node.node })));
|
||||
}
|
||||
|
||||
if (typeFilter === 'all' || typeFilter === 'lxc') {
|
||||
const nodeLXCs = await this.proxmoxRequest(`/nodes/${node.node}/lxc`);
|
||||
vms.push(...nodeLXCs.map(vm => ({ ...vm, type: 'lxc', node: vm.node || node.node })));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = '💻 **Virtual Machines**\n\n';
|
||||
|
||||
if (vms.length === 0) {
|
||||
output += 'No virtual machines found.\n';
|
||||
} else {
|
||||
for (const vm of vms.sort((a, b) => parseInt(a.vmid) - parseInt(b.vmid))) {
|
||||
const status = vm.status === 'running' ? '🟢' : vm.status === 'stopped' ? '🔴' : '🟡';
|
||||
const typeIcon = vm.type === 'qemu' ? '🖥️' : '📦';
|
||||
const uptime = vm.uptime ? this.formatUptime(vm.uptime) : 'N/A';
|
||||
const cpuUsage = vm.cpu ? `${(vm.cpu * 100).toFixed(1)}%` : 'N/A';
|
||||
const memUsage = vm.mem && vm.maxmem ?
|
||||
`${this.formatBytes(vm.mem)} / ${this.formatBytes(vm.maxmem)}` : 'N/A';
|
||||
|
||||
output += `${status} ${typeIcon} **${vm.name || `VM-${vm.vmid}`}** (ID: ${vm.vmid})\n`;
|
||||
output += ` • Node: ${vm.node}\n`;
|
||||
output += ` • Status: ${vm.status}\n`;
|
||||
output += ` • Type: ${vm.type.toUpperCase()}\n`;
|
||||
if (vm.status === 'running') {
|
||||
output += ` • Uptime: ${uptime}\n`;
|
||||
output += ` • CPU: ${cpuUsage}\n`;
|
||||
output += ` • Memory: ${memUsage}\n`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }]
|
||||
};
|
||||
}
|
||||
|
||||
async getVMStatus(node, vmid, type = 'qemu') {
|
||||
const vmStatus = await this.proxmoxRequest(`/nodes/${node}/${type}/${vmid}/status/current`);
|
||||
|
||||
const status = vmStatus.status === 'running' ? '🟢' : vmStatus.status === 'stopped' ? '🔴' : '🟡';
|
||||
const typeIcon = type === 'qemu' ? '🖥️' : '📦';
|
||||
|
||||
let output = `${status} ${typeIcon} **${vmStatus.name || `VM-${vmid}`}** (ID: ${vmid})\n\n`;
|
||||
output += `• **Node**: ${node}\n`;
|
||||
output += `• **Status**: ${vmStatus.status}\n`;
|
||||
output += `• **Type**: ${type.toUpperCase()}\n`;
|
||||
|
||||
if (vmStatus.status === 'running') {
|
||||
output += `• **Uptime**: ${vmStatus.uptime ? this.formatUptime(vmStatus.uptime) : 'N/A'}\n`;
|
||||
output += `• **CPU Usage**: ${vmStatus.cpu ? `${(vmStatus.cpu * 100).toFixed(1)}%` : 'N/A'}\n`;
|
||||
output += `• **Memory**: ${vmStatus.mem && vmStatus.maxmem ?
|
||||
`${this.formatBytes(vmStatus.mem)} / ${this.formatBytes(vmStatus.maxmem)} (${((vmStatus.mem / vmStatus.maxmem) * 100).toFixed(1)}%)` : 'N/A'}\n`;
|
||||
output += `• **Disk Read**: ${vmStatus.diskread ? this.formatBytes(vmStatus.diskread) : 'N/A'}\n`;
|
||||
output += `• **Disk Write**: ${vmStatus.diskwrite ? this.formatBytes(vmStatus.diskwrite) : 'N/A'}\n`;
|
||||
output += `• **Network In**: ${vmStatus.netin ? this.formatBytes(vmStatus.netin) : 'N/A'}\n`;
|
||||
output += `• **Network Out**: ${vmStatus.netout ? this.formatBytes(vmStatus.netout) : 'N/A'}\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }]
|
||||
};
|
||||
}
|
||||
|
||||
async executeVMCommand(node, vmid, command, type = 'qemu') {
|
||||
if (!this.allowElevated) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `⚠️ **VM Command Execution Requires Elevated Permissions**\n\nTo execute commands on VMs, set \`PROXMOX_ALLOW_ELEVATED=true\` in your .env file and ensure your API token has appropriate VM permissions.\n\n**Current permissions**: Basic (VM listing only)\n**Requested command**: \`${command}\``
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// For QEMU VMs, we need to use the guest agent
|
||||
if (type === 'qemu') {
|
||||
const result = await this.proxmoxRequest(`/nodes/${node}/qemu/${vmid}/agent/exec`, 'POST', {
|
||||
command: command
|
||||
});
|
||||
|
||||
let output = `💻 **Command executed on VM ${vmid}**\n\n`;
|
||||
output += `**Command**: \`${command}\`\n`;
|
||||
output += `**Result**: Command submitted to guest agent\n`;
|
||||
output += `**PID**: ${result.pid || 'N/A'}\n\n`;
|
||||
output += `*Note: Use guest agent status to check command completion*`;
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }]
|
||||
};
|
||||
} else {
|
||||
// For LXC containers, we can execute directly
|
||||
const result = await this.proxmoxRequest(`/nodes/${node}/lxc/${vmid}/exec`, 'POST', {
|
||||
command: command
|
||||
});
|
||||
|
||||
let output = `📦 **Command executed on LXC ${vmid}**\n\n`;
|
||||
output += `**Command**: \`${command}\`\n`;
|
||||
output += `**Output**:\n\`\`\`\n${result || 'Command executed successfully'}\n\`\`\``;
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }]
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `❌ **Failed to execute command on VM ${vmid}**\n\nError: ${error.message}\n\n*Note: Make sure the VM has guest agent installed and running*`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getStorage(nodeFilter = null) {
|
||||
let storages = [];
|
||||
|
||||
if (nodeFilter) {
|
||||
storages = await this.proxmoxRequest(`/nodes/${nodeFilter}/storage`);
|
||||
storages = storages.map(storage => ({ ...storage, node: nodeFilter }));
|
||||
} else {
|
||||
const nodes = await this.proxmoxRequest('/nodes');
|
||||
|
||||
for (const node of nodes) {
|
||||
const nodeStorages = await this.proxmoxRequest(`/nodes/${node.node}/storage`);
|
||||
storages.push(...nodeStorages.map(storage => ({ ...storage, node: node.node })));
|
||||
}
|
||||
}
|
||||
|
||||
let output = '💾 **Storage Pools**\n\n';
|
||||
|
||||
if (storages.length === 0) {
|
||||
output += 'No storage found.\n';
|
||||
} else {
|
||||
const uniqueStorages = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const storage of storages) {
|
||||
const key = `${storage.storage}-${storage.node}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
uniqueStorages.push(storage);
|
||||
}
|
||||
}
|
||||
|
||||
for (const storage of uniqueStorages.sort((a, b) => a.storage.localeCompare(b.storage))) {
|
||||
const enabled = storage.enabled ? '🟢' : '🔴';
|
||||
const usagePercent = storage.total && storage.used ?
|
||||
((storage.used / storage.total) * 100).toFixed(1) : 'N/A';
|
||||
|
||||
output += `${enabled} **${storage.storage}**\n`;
|
||||
output += ` • Node: ${storage.node}\n`;
|
||||
output += ` • Type: ${storage.type || 'N/A'}\n`;
|
||||
output += ` • Content: ${storage.content || 'N/A'}\n`;
|
||||
if (storage.total && storage.used) {
|
||||
output += ` • Usage: ${this.formatBytes(storage.used)} / ${this.formatBytes(storage.total)} (${usagePercent}%)\n`;
|
||||
}
|
||||
output += ` • Status: ${storage.enabled ? 'Enabled' : 'Disabled'}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }]
|
||||
};
|
||||
}
|
||||
|
||||
async getClusterStatus() {
|
||||
try {
|
||||
const nodes = await this.proxmoxRequest('/nodes');
|
||||
|
||||
// Try to get cluster status, but fall back gracefully if permissions are insufficient
|
||||
let clusterStatus = null;
|
||||
if (this.allowElevated) {
|
||||
try {
|
||||
clusterStatus = await this.proxmoxRequest('/cluster/status');
|
||||
} catch (error) {
|
||||
// Ignore cluster status errors for elevated permissions
|
||||
}
|
||||
}
|
||||
|
||||
let output = '🏗️ **Proxmox Cluster Status**\n\n';
|
||||
|
||||
// Cluster overview
|
||||
const onlineNodes = nodes.filter(n => n.status === 'online').length;
|
||||
const totalNodes = nodes.length;
|
||||
|
||||
output += `**Cluster Health**: ${onlineNodes === totalNodes ? '🟢 Healthy' : '🟡 Warning'}\n`;
|
||||
output += `**Nodes**: ${onlineNodes}/${totalNodes} online\n\n`;
|
||||
|
||||
if (this.allowElevated) {
|
||||
// Resource summary (only available with elevated permissions)
|
||||
let totalCpu = 0, usedCpu = 0;
|
||||
let totalMem = 0, usedMem = 0;
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.status === 'online') {
|
||||
totalCpu += node.maxcpu || 0;
|
||||
usedCpu += (node.cpu || 0) * (node.maxcpu || 0);
|
||||
totalMem += node.maxmem || 0;
|
||||
usedMem += node.mem || 0;
|
||||
}
|
||||
}
|
||||
|
||||
const cpuPercent = totalCpu > 0 ? ((usedCpu / totalCpu) * 100).toFixed(1) : 'N/A';
|
||||
const memPercent = totalMem > 0 ? ((usedMem / totalMem) * 100).toFixed(1) : 'N/A';
|
||||
|
||||
output += `**Resource Usage**:\n`;
|
||||
output += `• CPU: ${cpuPercent}% (${usedCpu.toFixed(1)}/${totalCpu} cores)\n`;
|
||||
output += `• Memory: ${memPercent}% (${this.formatBytes(usedMem)}/${this.formatBytes(totalMem)})\n\n`;
|
||||
} else {
|
||||
output += `⚠️ **Limited Information**: Resource usage requires elevated permissions\n\n`;
|
||||
}
|
||||
|
||||
// Node status
|
||||
output += `**Node Details**:\n`;
|
||||
for (const node of nodes.sort((a, b) => a.node.localeCompare(b.node))) {
|
||||
const status = node.status === 'online' ? '🟢' : '🔴';
|
||||
output += `${status} ${node.node} - ${node.status}\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: output }]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `❌ **Failed to get cluster status**\n\nError: ${error.message}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
formatUptime(seconds) {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('Proxmox MCP server running on stdio');
|
||||
}
|
||||
}
|
||||
|
||||
const server = new ProxmoxServer();
|
||||
server.run().catch(console.error);
|
||||
Reference in New Issue
Block a user