mcp-http-server.js•31.2 kB
#!/usr/bin/env node
/**
* VergeOS MCP Server - HTTP+SSE Transport
*
* This implements the MCP HTTP+SSE transport protocol for remote access.
* Compatible with clients that support the streamable HTTP MCP transport.
*/
import express from "express";
import cors from "cors";
import https from "https";
import { randomUUID } from "crypto";
// Load .env file if present
import { config } from "dotenv";
config();
const PORT = process.env.PORT || 3002;
const VERGEOS_HOST = process.env.VERGEOS_HOST || "192.168.1.111";
const VERGEOS_USER = process.env.VERGEOS_USER || "";
const VERGEOS_PASS = process.env.VERGEOS_PASS || "";
// Create HTTPS agent that ignores self-signed certs
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
// VergeOS API Client
class VergeOSAPI {
constructor() {
this.baseUrl = `https://${VERGEOS_HOST}`;
this.token = null;
}
async getToken() {
if (this.token) return this.token;
if (!VERGEOS_USER || !VERGEOS_PASS) {
throw new Error("VergeOS credentials not configured");
}
const response = await this.fetch("/api/sys/tokens", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Basic " + Buffer.from(`${VERGEOS_USER}:${VERGEOS_PASS}`).toString("base64"),
},
body: JSON.stringify({
login: VERGEOS_USER,
password: VERGEOS_PASS,
}),
});
const data = await response.json();
this.token = data.$key;
return this.token;
}
async fetch(path, options = {}) {
const url = `${this.baseUrl}${path}`;
const fetch = (await import("node-fetch")).default;
const fetchOptions = {
...options,
agent: httpsAgent,
headers: { ...options.headers },
};
if (this.token && !options.headers?.Authorization) {
fetchOptions.headers.Cookie = `token=${this.token}`;
}
return fetch(url, fetchOptions);
}
async request(path, options = {}) {
await this.getToken();
const response = await this.fetch(path, options);
if (!response.ok) {
const text = await response.text();
throw new Error(`API Error ${response.status}: ${text}`);
}
return response.json();
}
// VM Operations
async listVMs(filters = {}) {
// Get VMs and their runtime status
const [vms, statuses] = await Promise.all([
this.request("/api/v4/vms?fields=most"),
this.request("/api/v4/machine_status")
]);
// Create status lookup by machine ID
const statusMap = new Map(statuses.map(s => [s.machine, s]));
// Filter and map VMs
let result = vms
.filter((vm) => !vm.is_snapshot)
.map((vm) => {
const status = statusMap.get(vm.machine) || {};
return {
id: vm.$key,
name: vm.name,
machine: vm.machine,
enabled: vm.enabled,
running: status.running || false,
status: status.status || "unknown",
cpu_cores: vm.cpu_cores,
ram: vm.ram,
os_family: vm.os_family,
description: vm.description || "",
};
});
// Apply filters
if (filters.running === true) {
result = result.filter(vm => vm.running === true);
} else if (filters.running === false) {
result = result.filter(vm => vm.running === false);
}
if (filters.name) {
const nameLower = filters.name.toLowerCase();
result = result.filter(vm => vm.name.toLowerCase().includes(nameLower));
}
return result;
}
async getVM(id) {
// Get VM details and runtime status
const [vm, statuses] = await Promise.all([
this.request(`/api/v4/vms/${id}?fields=most`),
this.request(`/api/v4/machine_status`)
]);
const status = statuses.find(s => s.machine === vm.machine) || {};
// Add human-readable power state
return {
...vm,
power_state: status.status || "unknown",
running: status.running || false,
status_info: status.status_info || "",
migratable: status.migratable || false,
};
}
async getVMStatus(id) {
// Get VM to find machine ID, then get status
const vm = await this.request(`/api/v4/vms/${id}?fields=most`);
const statuses = await this.request("/api/v4/machine_status");
const status = statuses.find(s => s.machine === vm.machine);
if (!status) {
return { vm_id: id, name: vm.name, power_state: "unknown", running: false };
}
return {
vm_id: id,
name: vm.name,
machine: vm.machine,
running: status.running,
power_state: status.status,
status_info: status.status_info || "",
migratable: status.migratable,
};
}
async vmAction(id, action) {
return this.request("/api/v4/vm_actions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ vm: id, action }),
});
}
async powerOnVM(id) {
const status = await this.getVMStatus(id);
if (status.running) {
return { success: false, error: `VM '${status.name}' is already running`, current_state: status.power_state };
}
await this.vmAction(id, "poweron");
return { success: true, message: `Power on command sent to VM '${status.name}'`, previous_state: status.power_state };
}
async powerOffVM(id, options = {}) {
const { wait_timeout = 0, force_after_timeout = false } = options;
const status = await this.getVMStatus(id);
if (!status.running) {
return { success: true, message: `VM '${status.name}' is already stopped`, current_state: status.power_state, was_running: false };
}
// Send graceful shutdown
await this.vmAction(id, "poweroff");
// If no wait requested, return immediately
if (wait_timeout <= 0) {
return {
success: true,
message: `Graceful shutdown command sent to VM '${status.name}'`,
previous_state: status.power_state,
note: "Use wait_timeout parameter to wait for shutdown completion"
};
}
// Poll for shutdown with timeout
const startTime = Date.now();
const pollInterval = 3000; // 3 seconds
const maxWait = Math.min(wait_timeout, 300) * 1000; // Cap at 5 minutes
while (Date.now() - startTime < maxWait) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
const currentStatus = await this.getVMStatus(id);
if (!currentStatus.running) {
return {
success: true,
message: `VM '${status.name}' shut down gracefully`,
final_state: currentStatus.power_state,
elapsed_seconds: Math.round((Date.now() - startTime) / 1000)
};
}
}
// Timeout reached - check if we should force
if (force_after_timeout) {
await this.vmAction(id, "kill");
// Wait a moment for kill to take effect
await new Promise(resolve => setTimeout(resolve, 2000));
const finalStatus = await this.getVMStatus(id);
return {
success: true,
message: `VM '${status.name}' did not shut down within ${wait_timeout}s - forced power off`,
final_state: finalStatus.power_state,
forced: true,
elapsed_seconds: Math.round((Date.now() - startTime) / 1000)
};
}
// Timeout without force
const currentStatus = await this.getVMStatus(id);
return {
success: false,
error: `VM '${status.name}' did not shut down within ${wait_timeout}s`,
current_state: currentStatus.power_state,
elapsed_seconds: Math.round((Date.now() - startTime) / 1000),
hint: "Use force_after_timeout=true to automatically force shutdown after timeout"
};
}
async forceOffVM(id) {
const status = await this.getVMStatus(id);
if (!status.running) {
return { success: true, message: `VM '${status.name}' is already stopped`, current_state: status.power_state };
}
await this.vmAction(id, "kill");
return { success: true, message: `Force power off (kill) command sent to VM '${status.name}'`, previous_state: status.power_state };
}
async resetVM(id) {
const status = await this.getVMStatus(id);
if (!status.running) {
return { success: false, error: `VM '${status.name}' is not running - cannot reset`, current_state: status.power_state };
}
await this.vmAction(id, "reset");
return { success: true, message: `Reset command sent to VM '${status.name}'`, previous_state: status.power_state };
}
async getVMNics(machineId) {
const nics = await this.request(`/api/v4/machine_nics?machine=${machineId}&fields=most`);
return nics.filter((nic) => nic.machine === machineId);
}
async getVMDrives(machineId) {
const drives = await this.request(`/api/v4/machine_drives?machine=${machineId}&fields=all`);
// Filter by machine ID and return simplified info
return drives
.filter((d) => d.machine === machineId)
.map((d) => ({
id: d.$key,
name: d.name,
interface: d.interface,
size_gb: d.disksize ? Math.round(d.disksize / (1024 * 1024 * 1024)) : null,
size_bytes: d.disksize || null,
enabled: d.enabled,
media_type: d.media_type,
description: d.description || "",
}));
}
async resizeDrive(driveId, newSizeGB) {
// Get current drive info
const drive = await this.request(`/api/v4/machine_drives/${driveId}?fields=all`);
if (!drive) {
return { success: false, error: `Drive ${driveId} not found` };
}
const currentSizeGB = Math.round(drive.disksize / (1024 * 1024 * 1024));
const newSizeBytes = newSizeGB * 1024 * 1024 * 1024;
if (newSizeGB <= currentSizeGB) {
return {
success: false,
error: `New size (${newSizeGB} GB) must be larger than current size (${currentSizeGB} GB). Shrinking disks is not supported.`,
current_size_gb: currentSizeGB
};
}
// Resize the drive
await this.request(`/api/v4/machine_drives/${driveId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ disksize: newSizeBytes }),
});
return {
success: true,
message: `Drive '${drive.name}' resized from ${currentSizeGB} GB to ${newSizeGB} GB`,
drive_id: driveId,
previous_size_gb: currentSizeGB,
new_size_gb: newSizeGB,
note: "You may need to extend the partition/filesystem inside the VM to use the new space"
};
}
async addDrive(machineId, options = {}) {
const { name, size_gb, interface_type = "virtio-scsi", description = "" } = options;
if (!name) {
return { success: false, error: "Drive name is required" };
}
if (!size_gb || size_gb < 1) {
return { success: false, error: "Size in GB is required and must be at least 1 GB" };
}
const sizeBytes = size_gb * 1024 * 1024 * 1024;
// Create the drive
const result = await this.request("/api/v4/machine_drives", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
machine: machineId,
name: name,
disksize: sizeBytes,
interface: interface_type,
media: "disk",
description: description,
enabled: true,
}),
});
return {
success: true,
message: `Drive '${name}' (${size_gb} GB) added to machine ${machineId}`,
drive_id: result.$key,
name: name,
size_gb: size_gb,
interface: interface_type,
note: "The VM may need to be restarted to detect the new drive"
};
}
async modifyVM(vmId, options = {}) {
const { cpu_cores, ram_mb, shutdown_if_running = false, wait_timeout = 60, force_after_timeout = true } = options;
if (!cpu_cores && !ram_mb) {
return { success: false, error: "Must specify cpu_cores and/or ram_mb to modify" };
}
// Get current VM info and status
const vm = await this.getVM(vmId);
const status = await this.getVMStatus(vmId);
const changes = {};
if (cpu_cores) changes.cpu_cores = cpu_cores;
if (ram_mb) changes.ram = ram_mb;
// Check if VM is running
if (status.running) {
if (!shutdown_if_running) {
return {
success: false,
error: `VM '${vm.name}' is currently running. CPU/RAM changes require the VM to be powered off.`,
current_state: status.power_state,
current_cpu: vm.cpu_cores,
current_ram_mb: vm.ram,
requested_cpu: cpu_cores || vm.cpu_cores,
requested_ram_mb: ram_mb || vm.ram,
hint: "Set shutdown_if_running=true to automatically shut down the VM, apply changes, and optionally restart it"
};
}
// Shut down the VM
const shutdownResult = await this.powerOffVM(vmId, { wait_timeout, force_after_timeout });
if (!shutdownResult.success && !shutdownResult.message?.includes("already stopped")) {
return {
success: false,
error: `Failed to shut down VM '${vm.name}': ${shutdownResult.error}`,
shutdown_result: shutdownResult
};
}
}
// Apply changes
await this.request(`/api/v4/vms/${vmId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(changes),
});
return {
success: true,
message: `VM '${vm.name}' modified successfully`,
vm_id: vmId,
previous_cpu: vm.cpu_cores,
previous_ram_mb: vm.ram,
new_cpu: cpu_cores || vm.cpu_cores,
new_ram_mb: ram_mb || vm.ram,
was_running: status.running,
note: status.running ? "VM was shut down to apply changes. Use power_on_vm to restart it." : "VM is stopped. Use power_on_vm to start it with new settings."
};
}
// Network Operations
async listNetworks(options = {}) {
const { type, name, enabled, limit = 100, offset = 0 } = options;
const networks = await this.request("/api/v4/vnets?fields=most");
// Filter
let filtered = networks;
if (type) filtered = filtered.filter(n => n.type === type);
if (name) filtered = filtered.filter(n => n.name?.toLowerCase().includes(name.toLowerCase()));
if (enabled !== undefined) filtered = filtered.filter(n => n.enabled === enabled);
// Paginate and return summary view
return filtered.slice(offset, offset + limit).map(n => ({
id: n.$key,
name: n.name,
type: n.type,
network: n.network,
enabled: n.enabled,
running: n.running,
description: n.description || null,
}));
}
async getNetwork(id) { return this.request(`/api/v4/vnets/${id}?fields=most`); }
async networkAction(id, action) {
return this.request("/api/v4/vnet_actions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ vnet: id, action }),
});
}
// Tenant Operations
async listTenants() { return this.request("/api/v4/tenants?fields=most"); }
async getTenant(id) { return this.request(`/api/v4/tenants/${id}?fields=most`); }
async tenantAction(id, action) {
return this.request("/api/v4/tenant_actions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tenant: id, action }),
});
}
// Node/Cluster Operations
async listNodes() { return this.request("/api/v4/nodes?fields=most"); }
async getNodeStats(id) { return this.request(`/api/v4/node_stats?node=${id}`); }
async getClusterStatus() { return this.request("/api/v4/cluster_status"); }
async getClusterStats() { return this.request("/api/v4/cluster_tier_stats"); }
// Storage & Monitoring
async listVolumes() { return this.request("/api/v4/volumes?fields=most"); }
async getLogs(options = {}) {
const { limit = 50, level, object_type } = options;
// Fetch more logs if filtering, then apply client-side filter
const fetchLimit = (level || object_type) ? Math.min(limit * 5, 500) : limit;
const logs = await this.request(`/api/v4/logs?fields=all&limit=${fetchLimit}&sort=-$key`);
let filtered = logs;
if (level) filtered = filtered.filter(log => log.level === level);
if (object_type) filtered = filtered.filter(log => log.object_type === object_type);
return filtered.slice(0, limit).map(log => ({
id: log.$key,
timestamp: log.dbtime,
level: log.level,
text: log.text,
user: log.user,
object_type: log.object_type,
object_name: log.object_name,
}));
}
async getAlarms() { return this.request("/api/v4/alarms?fields=most"); }
}
// Initialize API
const api = new VergeOSAPI();
// MCP Tools Definition
const TOOLS = [
{ name: "list_vms", description: "List all virtual machines in VergeOS. Can filter by running status or name.", inputSchema: { type: "object", properties: { running: { type: "boolean", description: "Filter to only running VMs" }, name: { type: "string", description: "Filter by VM name" } } } },
{ name: "get_vm", description: "Get detailed information about a specific VM by ID", inputSchema: { type: "object", properties: { id: { type: "number", description: "VM ID" } }, required: ["id"] } },
{ name: "get_vm_status", description: "Get the current status of a VM", inputSchema: { type: "object", properties: { id: { type: "number", description: "VM ID" } }, required: ["id"] } },
{ name: "power_on_vm", description: "Power on a virtual machine", inputSchema: { type: "object", properties: { id: { type: "number", description: "VM ID" } }, required: ["id"] } },
{ name: "power_off_vm", description: "Power off a virtual machine (graceful shutdown). Use wait_timeout to wait for completion, and force_after_timeout to auto-force if graceful shutdown fails.", inputSchema: { type: "object", properties: { id: { type: "number", description: "VM ID" }, wait_timeout: { type: "number", description: "Seconds to wait for graceful shutdown (0 = don't wait, max 300). Recommended: 60-120 for most VMs." }, force_after_timeout: { type: "boolean", description: "If true, force power off after wait_timeout expires. Recommended: true for reliable shutdown." } }, required: ["id"] } },
{ name: "force_off_vm", description: "Force power off a VM (hard shutdown - use when graceful shutdown fails)", inputSchema: { type: "object", properties: { id: { type: "number", description: "VM ID" } }, required: ["id"] } },
{ name: "reset_vm", description: "Reset/reboot a virtual machine", inputSchema: { type: "object", properties: { id: { type: "number", description: "VM ID" } }, required: ["id"] } },
{ name: "get_vm_nics", description: "Get network interfaces for a VM", inputSchema: { type: "object", properties: { id: { type: "number", description: "VM ID" } }, required: ["id"] } },
{ name: "get_vm_drives", description: "Get disk drives for a VM (use machine ID, not VM ID)", inputSchema: { type: "object", properties: { id: { type: "number", description: "Machine ID (from VM's 'machine' field)" } }, required: ["id"] } },
{ name: "resize_drive", description: "Resize a VM disk drive (increase only). Get drive IDs from get_vm_drives first.", inputSchema: { type: "object", properties: { drive_id: { type: "number", description: "Drive ID (from get_vm_drives)" }, new_size_gb: { type: "number", description: "New size in GB (must be larger than current size)" } }, required: ["drive_id", "new_size_gb"] } },
{ name: "add_drive", description: "Add a new disk drive to a VM. Use machine ID (from VM's 'machine' field), not VM ID.", inputSchema: { type: "object", properties: { machine_id: { type: "number", description: "Machine ID (from VM's 'machine' field)" }, name: { type: "string", description: "Drive name (e.g., 'data-disk')" }, size_gb: { type: "number", description: "Size in GB" }, interface_type: { type: "string", enum: ["virtio-scsi", "virtio", "ide", "ahci"], description: "Interface type (default: virtio-scsi)" }, description: { type: "string", description: "Optional description" } }, required: ["machine_id", "name", "size_gb"] } },
{ name: "modify_vm", description: "Modify VM CPU cores and/or RAM. If VM is running, set shutdown_if_running=true to auto-shutdown, apply changes, then you can restart.", inputSchema: { type: "object", properties: { id: { type: "number", description: "VM ID" }, cpu_cores: { type: "number", description: "New number of CPU cores" }, ram_mb: { type: "number", description: "New RAM in MB (e.g., 4096 for 4GB)" }, shutdown_if_running: { type: "boolean", description: "If true and VM is running, shut it down first to apply changes" }, wait_timeout: { type: "number", description: "Seconds to wait for shutdown (default: 60)" }, force_after_timeout: { type: "boolean", description: "Force shutdown if graceful fails (default: true)" } }, required: ["id"] } },
{ name: "list_networks", description: "List virtual networks (summary view). Use get_network for full details.", inputSchema: { type: "object", properties: { type: { type: "string", description: "Filter by network type (e.g., 'internal', 'external', 'core', 'dmz')" }, name: { type: "string", description: "Filter by name (partial match)" }, enabled: { type: "boolean", description: "Filter by enabled status" }, limit: { type: "number", description: "Max results (default 100)" }, offset: { type: "number", description: "Skip first N results (for pagination)" } } } },
{ name: "get_network", description: "Get network details", inputSchema: { type: "object", properties: { id: { type: "number", description: "Network ID" } }, required: ["id"] } },
{ name: "network_action", description: "Perform network action (poweron, poweroff, reset, apply)", inputSchema: { type: "object", properties: { id: { type: "number" }, action: { type: "string", enum: ["poweron", "poweroff", "reset", "apply"] } }, required: ["id", "action"] } },
{ name: "list_tenants", description: "List all tenants", inputSchema: { type: "object", properties: {} } },
{ name: "get_tenant", description: "Get tenant details", inputSchema: { type: "object", properties: { id: { type: "number" } }, required: ["id"] } },
{ name: "tenant_action", description: "Perform tenant action", inputSchema: { type: "object", properties: { id: { type: "number" }, action: { type: "string", enum: ["poweron", "poweroff", "reset"] } }, required: ["id", "action"] } },
{ name: "list_nodes", description: "List cluster nodes", inputSchema: { type: "object", properties: {} } },
{ name: "get_node_stats", description: "Get node statistics", inputSchema: { type: "object", properties: { id: { type: "number" } }, required: ["id"] } },
{ name: "get_cluster_status", description: "Get cluster status", inputSchema: { type: "object", properties: {} } },
{ name: "get_cluster_stats", description: "Get cluster tier stats", inputSchema: { type: "object", properties: {} } },
{ name: "list_volumes", description: "List storage volumes", inputSchema: { type: "object", properties: {} } },
{ name: "get_logs", description: "Get system logs with optional filtering", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Number of logs (default 50)" }, level: { type: "string", enum: ["audit", "message", "warning", "error", "critical", "summary", "debug"], description: "Filter by log level" }, object_type: { type: "string", enum: ["vm", "vnet", "tenant", "node", "cluster", "user", "system", "task"], description: "Filter by object type" } } } },
{ name: "get_alarms", description: "Get active alarms", inputSchema: { type: "object", properties: {} } },
];
// Execute tool
async function executeTool(name, args) {
switch (name) {
case "list_vms": return api.listVMs(args);
case "get_vm": return api.getVM(args.id);
case "get_vm_status": return api.getVMStatus(args.id);
case "power_on_vm": return api.powerOnVM(args.id);
case "power_off_vm": return api.powerOffVM(args.id, { wait_timeout: args.wait_timeout, force_after_timeout: args.force_after_timeout });
case "force_off_vm": return api.forceOffVM(args.id);
case "reset_vm": return api.resetVM(args.id);
case "get_vm_nics": return api.getVMNics(args.id);
case "get_vm_drives": return api.getVMDrives(args.id);
case "resize_drive": return api.resizeDrive(args.drive_id, args.new_size_gb);
case "add_drive": return api.addDrive(args.machine_id, { name: args.name, size_gb: args.size_gb, interface_type: args.interface_type, description: args.description });
case "modify_vm": return api.modifyVM(args.id, { cpu_cores: args.cpu_cores, ram_mb: args.ram_mb, shutdown_if_running: args.shutdown_if_running, wait_timeout: args.wait_timeout, force_after_timeout: args.force_after_timeout });
case "list_networks": return api.listNetworks({ type: args.type, name: args.name, enabled: args.enabled, limit: args.limit, offset: args.offset });
case "get_network": return api.getNetwork(args.id);
case "network_action": return api.networkAction(args.id, args.action);
case "list_tenants": return api.listTenants();
case "get_tenant": return api.getTenant(args.id);
case "tenant_action": return api.tenantAction(args.id, args.action);
case "list_nodes": return api.listNodes();
case "get_node_stats": return api.getNodeStats(args.id);
case "get_cluster_status": return api.getClusterStatus();
case "get_cluster_stats": return api.getClusterStats();
case "list_volumes": return api.listVolumes();
case "get_logs": return api.getLogs({ limit: args?.limit || 50, level: args?.level, object_type: args?.object_type });
case "get_alarms": return api.getAlarms();
default: throw new Error(`Unknown tool: ${name}`);
}
}
// Create Express app
const app = express();
app.use(cors());
app.use(express.json());
// Store active SSE sessions
const sessions = new Map();
// Health check
app.get("/health", (req, res) => {
res.json({ status: "ok", server: "vergeos-mcp-server", vergeosHost: VERGEOS_HOST });
});
// MCP SSE endpoint - for establishing SSE connection
app.get("/sse", (req, res) => {
const sessionId = randomUUID();
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Session-Id", sessionId);
// Send session ID
res.write(`data: ${JSON.stringify({ type: "session", sessionId })}\n\n`);
// Store session
sessions.set(sessionId, res);
// Cleanup on close
req.on("close", () => {
sessions.delete(sessionId);
});
});
// MCP message endpoint - for receiving requests
app.post("/message", async (req, res) => {
const message = req.body;
try {
let response;
switch (message.method) {
case "initialize":
response = {
jsonrpc: "2.0",
id: message.id,
result: {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
serverInfo: { name: "vergeos-mcp-server", version: "1.0.0" },
},
};
break;
case "tools/list":
response = {
jsonrpc: "2.0",
id: message.id,
result: { tools: TOOLS },
};
break;
case "tools/call":
try {
const result = await executeTool(message.params.name, message.params.arguments || {});
response = {
jsonrpc: "2.0",
id: message.id,
result: {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
},
};
} catch (error) {
response = {
jsonrpc: "2.0",
id: message.id,
result: {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true,
},
};
}
break;
case "resources/list":
response = {
jsonrpc: "2.0",
id: message.id,
result: {
resources: [
{ uri: "vergeos://cluster/status", name: "Cluster Status", mimeType: "application/json" },
{ uri: "vergeos://vms/list", name: "Virtual Machines", mimeType: "application/json" },
],
},
};
break;
default:
response = {
jsonrpc: "2.0",
id: message.id,
error: { code: -32601, message: `Method not found: ${message.method}` },
};
}
res.json(response);
} catch (error) {
res.status(500).json({
jsonrpc: "2.0",
id: message.id,
error: { code: -32603, message: error.message },
});
}
});
// Legacy REST endpoints (for direct API access)
app.get("/tools", (req, res) => res.json({ tools: TOOLS }));
app.post("/tools/:name", async (req, res) => {
try {
const result = await executeTool(req.params.name, req.body || {});
res.json({ result });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get("/vms", async (req, res) => {
try { res.json(await api.listVMs(req.query)); } catch (e) { res.status(500).json({ error: e.message }); }
});
app.get("/vms/:id", async (req, res) => {
try { res.json(await api.getVM(parseInt(req.params.id))); } catch (e) { res.status(500).json({ error: e.message }); }
});
app.post("/vms/:id/:action", async (req, res) => {
try { res.json(await api.vmAction(parseInt(req.params.id), req.params.action)); } catch (e) { res.status(500).json({ error: e.message }); }
});
app.get("/networks", async (req, res) => {
try { res.json(await api.listNetworks()); } catch (e) { res.status(500).json({ error: e.message }); }
});
app.get("/tenants", async (req, res) => {
try { res.json(await api.listTenants()); } catch (e) { res.status(500).json({ error: e.message }); }
});
app.get("/nodes", async (req, res) => {
try { res.json(await api.listNodes()); } catch (e) { res.status(500).json({ error: e.message }); }
});
app.get("/cluster/status", async (req, res) => {
try { res.json(await api.getClusterStatus()); } catch (e) { res.status(500).json({ error: e.message }); }
});
app.get("/alarms", async (req, res) => {
try { res.json(await api.getAlarms()); } catch (e) { res.status(500).json({ error: e.message }); }
});
app.get("/logs", async (req, res) => {
try { res.json(await api.getLogs(parseInt(req.query.limit) || 50)); } catch (e) { res.status(500).json({ error: e.message }); }
});
// Start server
app.listen(PORT, () => {
console.log("===========================================");
console.log("VergeOS MCP Server (HTTP+SSE Mode)");
console.log("===========================================");
console.log(`Server running on: http://localhost:${PORT}`);
console.log(`VergeOS endpoint: https://${VERGEOS_HOST}`);
console.log("");
console.log("MCP Endpoints:");
console.log(" GET /sse - SSE connection");
console.log(" POST /message - MCP JSON-RPC messages");
console.log("");
console.log("REST Endpoints:");
console.log(" GET /health, /tools, /vms, /networks, etc.");
console.log("===========================================");
});