We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmagar/homelab-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
import type { HostConfig } from "../types.js";
import { SSHCommandError } from "../utils/errors.js";
import { escapeShellArg } from "../utils/path-security.js";
import type { ISSHConnectionPool, ISSHService } from "./interfaces.js";
import type { HostResources } from "./ssh.js";
/**
* Hardcoded shell script for collecting host resource information.
*
* Security (S-H4): This multi-command script is SAFE because it is fully hardcoded
* with no user input. The script pattern (joining with semicolons) is only safe
* for hardcoded commands.
*
* WARNING: Do NOT copy this pattern for user-influenced commands. User input
* must be validated and escaped per command using escapeShellArg().
*
* @see https://cwe.mitre.org/data/definitions/78.html
*/
const HOST_RESOURCES_SCRIPT = `
hostname
echo "---"
uptime -p 2>/dev/null || uptime | sed 's/.*up/up/'
echo "---"
cat /proc/loadavg | awk '{print $1,$2,$3}'
echo "---"
nproc
echo "---"
top -bn1 | grep "Cpu(s)" | awk '{print 100-$8}' 2>/dev/null || echo "0"
echo "---"
free -m | awk '/^Mem:/ {print $2,$3,$4}'
echo "---"
df -BG --output=source,target,size,used,avail,pcent 2>/dev/null | grep -E '^/dev' || df -h | grep -E '^/dev'
`
.trim()
.replace(/\n/g, "; ");
/**
* Options for SSH command execution
*/
export interface SSHCommandOptions {
timeoutMs?: number; // Command timeout (default: 30000)
}
/**
* SSH service implementation using connection pool for command execution.
* Provides secure command execution and resource monitoring via SSH connections.
*/
export class SSHService implements ISSHService {
constructor(private readonly pool: ISSHConnectionPool) {}
/**
* Execute SSH command using connection pool
*
* Automatically acquires connection from pool, executes command, and releases.
* Connections are reused across calls for better performance.
*
* @param host - Host configuration
* @param command - Command to execute
* @param args - Command arguments (optional)
* @param options - Execution options (timeout, etc.)
* @returns Command stdout (trimmed)
* @throws SSHCommandError if command fails or times out
*/
async executeSSHCommand(
host: HostConfig,
command: string,
args: string[] = [],
options: SSHCommandOptions = {}
): Promise<string> {
const timeoutMs = options.timeoutMs || 30000;
// Get connection from pool
const connection = await this.pool.getConnection(host);
// Build full command with escaped arguments to prevent shell injection
const fullCommand =
args.length > 0 ? `${command} ${args.map((a) => escapeShellArg(a)).join(" ")}` : command;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
try {
// Execute with timeout
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`SSH command timeout after ${timeoutMs}ms: ${command}`));
}, timeoutMs);
});
const execPromise = connection.execCommand(fullCommand);
const result = await Promise.race([execPromise, timeoutPromise]);
// Check exit code
if (result.code !== 0) {
throw new SSHCommandError(
"SSH command failed with non-zero exit code",
host.name,
fullCommand,
result.code ?? undefined,
result.stderr,
result.stdout
);
}
return result.stdout.trim();
} catch (error) {
if (error instanceof Error) {
if (error instanceof SSHCommandError) {
throw error;
}
const baseMessage = error.message || "SSH command execution failed";
throw new SSHCommandError(
baseMessage,
host.name,
fullCommand,
undefined,
undefined,
undefined,
error
);
}
throw new SSHCommandError(
"SSH command execution failed",
host.name,
fullCommand,
undefined,
undefined,
undefined,
error
);
} finally {
// Clear timeout to prevent resource leak
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// Always release connection back to pool
await this.pool.releaseConnection(host, connection);
}
}
/**
* Get host resource usage via SSH using connection pool
*
* Security (S-H4): Uses hardcoded HOST_RESOURCES_SCRIPT constant for safety.
* See constant definition for security documentation.
*
* @param host - Host configuration to query
* @returns Resource information including CPU, memory, disk, and uptime
*/
async getHostResources(host: HostConfig): Promise<HostResources> {
// Run all commands in one SSH session for efficiency
// SECURITY (S-H4): Script is hardcoded constant - safe for semicolon joining
const output = await this.executeSSHCommand(host, HOST_RESOURCES_SCRIPT);
const sections = output.split("---").map((s) => s.trim());
// Parse hostname
const hostname = sections[0] || host.name;
// Parse uptime
const uptime = sections[1] || "unknown";
// Parse load average
const loadParts = (sections[2] || "0 0 0").split(" ").map(Number);
const loadAverage: [number, number, number] = [
loadParts[0] || 0,
loadParts[1] || 0,
loadParts[2] || 0,
];
// Parse CPU
const cores = Number.parseInt(sections[3] || "1", 10);
const cpuUsage = Number.parseFloat(sections[4] || "0");
// Parse memory
const memParts = (sections[5] || "0 0 0").split(" ").map(Number);
const totalMB = memParts[0] || 0;
const usedMB = memParts[1] || 0;
const freeMB = memParts[2] || 0;
const memUsagePercent = totalMB > 0 ? (usedMB / totalMB) * 100 : 0;
// Parse disk
const diskLines = (sections[6] || "").split("\n").filter((l) => l.trim());
const disk = diskLines
.map((line) => {
const parts = line.trim().split(/\s+/);
if (parts.length >= 6) {
return {
filesystem: parts[0],
mount: parts[1],
totalGB: Number.parseFloat(parts[2].replace("G", "")) || 0,
usedGB: Number.parseFloat(parts[3].replace("G", "")) || 0,
availGB: Number.parseFloat(parts[4].replace("G", "")) || 0,
usagePercent: Number.parseFloat(parts[5].replace("%", "")) || 0,
};
}
return null;
})
.filter((d): d is NonNullable<typeof d> => d !== null);
return {
hostname,
uptime,
loadAverage,
cpu: {
cores,
usagePercent: Math.round(cpuUsage * 10) / 10,
},
memory: {
totalMB,
usedMB,
freeMB,
usagePercent: Math.round(memUsagePercent * 10) / 10,
},
disk,
};
}
}