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
// src/tools/handlers/host.ts
import type { ServiceContainer } from '../../services/container.js';
import type { FluxInput } from '../../schemas/flux/index.js';
import type { HostConfig } from '../../types.js';
import { loadHostConfigs } from '../../services/docker.js';
import { formatHostStatusMarkdown, formatHostResourcesMarkdown } from '../../formatters/index.js';
import { escapeShellArg, validateSSHArg, validateSystemdServiceName, isLocalHost } from '../../utils/index.js';
import { logError } from '../../utils/errors.js';
import {
assertValidAction,
getHostNameFromInput,
getOptionalString,
getHostConfigOptional,
initializeHandler,
formatResponse,
resolveTargetHosts,
createExhaustiveError
} from './base-handler.js';
/**
* Execute command on local or remote host based on configuration
* Routes to LocalExecutor for localhost, SSH for remote hosts
*/
async function executeCommand(
host: HostConfig,
command: string,
args: string[],
container: ServiceContainer
): Promise<string> {
if (isLocalHost(host)) {
const localExecutor = container.getLocalExecutor();
return await localExecutor.executeLocalCommand(command, args);
} else {
const sshService = container.getSSHService();
return await sshService.executeSSHCommand(host, command, args);
}
}
/**
* Handle all host subactions
*
* Subactions: status, resources, info, uptime, services, network, mounts
*/
export async function handleHostAction(
input: FluxInput,
container: ServiceContainer
): Promise<string> {
assertValidAction(input, 'host', 'host');
const hostName = getHostNameFromInput(input as unknown as Record<string, unknown>);
const { service: dockerService, hosts, format } = initializeHandler(
input,
container,
(c) => c.getDockerService(),
loadHostConfigs()
);
const sshService = container.getSSHService();
// Find the target host (can query all hosts if not specified for some actions)
const hostConfig = getHostConfigOptional(hosts, hostName);
// For most operations, require the host
if (hostName && !hostConfig) {
throw new Error(`Host not found: ${hostName}`);
}
switch (input.subaction) {
case 'status': {
// For status, we can check all hosts or specific host
const targetHosts = resolveTargetHosts(hosts, hostName);
const statusResults = await Promise.all(
targetHosts.map(async (h) => {
try {
const info = await dockerService.getDockerInfo(h);
const containers = await dockerService.listContainers([h]);
const runningCount = containers.filter(c => c.state === 'running').length;
return {
name: h.name,
connected: true,
containerCount: containers.length,
runningCount,
dockerVersion: info.dockerVersion,
error: undefined
};
} catch (err) {
logError(err, {
operation: 'handleHostAction:status',
metadata: { host: h.name, action: 'status' }
});
return {
name: h.name,
connected: false,
containerCount: 0,
runningCount: 0,
dockerVersion: undefined,
error: err instanceof Error ? err.message : 'Unknown error'
};
}
})
);
return formatResponse(
hostConfig ? statusResults[0] : statusResults,
format,
() => formatHostStatusMarkdown(statusResults)
);
}
case 'resources': {
// Resources requires SSH, must have a specific host
const targetHosts = resolveTargetHosts(hosts, hostName);
const resourceResults = await Promise.all(
targetHosts.map(async (h) => {
try {
const resources = await sshService.getHostResources(h);
return { host: h.name, resources, error: undefined };
} catch (err) {
logError(err, {
operation: 'handleHostAction:resources',
metadata: { host: h.name, action: 'resources' }
});
return {
host: h.name,
resources: null,
error: err instanceof Error ? err.message : 'Unknown error'
};
}
})
);
return formatResponse(
hostConfig ? resourceResults[0] : resourceResults,
format,
() => formatHostResourcesMarkdown(resourceResults)
);
}
case 'info': {
if (!hostConfig) {
throw new Error('Host is required for host:info');
}
const output = await executeCommand(hostConfig, 'uname', ['-a'], container);
const infoResult = { host: hostConfig.name, info: output.trim() };
return formatResponse(
infoResult,
format,
(data) => `## System Info - ${data.host}\n\n\`\`\`\n${data.info}\n\`\`\``
);
}
case 'uptime': {
if (!hostConfig) {
throw new Error('Host is required for host:uptime');
}
const output = await executeCommand(hostConfig, 'uptime', [], container);
const uptimeResult = { host: hostConfig.name, uptime: output.trim() };
return formatResponse(
uptimeResult,
format,
(data) => `## Uptime - ${data.host}\n\n\`\`\`\n${data.uptime}\n\`\`\``
);
}
case 'services': {
if (!hostConfig) {
throw new Error('Host is required for host:services');
}
const state = getOptionalString(input.state, 'state');
const service = getOptionalString(input.service, 'service');
// SECURITY: Validate user-provided parameters to prevent command injection
// The SSH service joins args with spaces and executes as shell command,
// so we must reject shell metacharacters like ; | & ` $ etc.
if (state && state !== 'all') {
validateSSHArg(state, 'state');
}
if (service) {
validateSystemdServiceName(service);
}
// Build systemctl command based on options
const args = ['list-units', '--type=service', '--no-pager'];
if (state && state !== 'all') {
const safeState = escapeShellArg(state);
args.push(`--state=${safeState}`);
}
if (service) {
const safeService = escapeShellArg(service);
args.push(safeService);
}
const output = await executeCommand(hostConfig, 'systemctl', args, container);
const servicesResult = { host: hostConfig.name, services: output.trim() };
return formatResponse(
servicesResult,
format,
(data) => `## Systemd Services - ${data.host}\n\n\`\`\`\n${data.services}\n\`\`\``
);
}
case 'network': {
if (!hostConfig) {
throw new Error('Host is required for host:network');
}
// Use ip addr or ifconfig
let output: string;
try {
output = await executeCommand(hostConfig, 'ip', ['addr', 'show'], container);
} catch {
// Fallback to ifconfig if ip command not available
output = await executeCommand(hostConfig, 'ifconfig', ['-a'], container);
}
const networkResult = { host: hostConfig.name, network: output.trim() };
return formatResponse(
networkResult,
format,
(data) => `## Network Interfaces - ${data.host}\n\n\`\`\`\n${data.network}\n\`\`\``
);
}
case 'mounts': {
if (!hostConfig) {
throw new Error('Host is required for host:mounts');
}
const output = await executeCommand(hostConfig, 'df', ['-h'], container);
const mountsResult = { host: hostConfig.name, mounts: output.trim() };
return formatResponse(
mountsResult,
format,
(data) => `## Mounted Filesystems - ${data.host}\n\n\`\`\`\n${data.mounts}\n\`\`\``
);
}
default:
throw createExhaustiveError(input as never, 'host subaction');
}
}