Skip to main content
Glama

homelab

Manage Docker containers, images, and compose projects across multiple homelab hosts. Monitor resources, retrieve logs, and execute remote file operations via SSH from a unified interface.

Instructions

Unified homelab Docker management tool.

ACTIONS: container - Container operations list - List containers with filters start/stop/restart - Control container state pause/unpause - Pause/unpause container logs - Get container logs stats - Get resource usage stats inspect - Get detailed container info search - Search containers by query pull - Pull latest image for container recreate - Recreate container with latest image

compose - Docker Compose operations list - List compose projects status - Get project status up/down/restart - Control project state logs - Get project logs build - Build project images pull - Pull project images recreate - Force recreate containers

host - Host operations status - Check host connectivity resources - Get CPU/memory/disk via SSH

docker - Docker daemon operations (host parameter required) info - Get Docker system info df - Get disk usage prune - Remove unused resources

image - Image operations list - List images pull - Pull an image build - Build from Dockerfile remove - Remove an image

scout - Remote file operations via SSH read - Read file content list - List directory contents tree - Show directory tree exec - Execute command find - Find files by pattern transfer - Transfer file between hosts diff - Diff files across hosts

EXAMPLES: { action: "container", subaction: "list", state: "running" } { action: "container", subaction: "restart", container_id: "plex" } { action: "compose", subaction: "up", host: "tootie", project: "plex" } { action: "host", subaction: "resources", host: "tootie" } { action: "docker", subaction: "info", host: "tootie" } { action: "docker", subaction: "df", host: "tootie" } { action: "docker", subaction: "prune", host: "tootie", prune_target: "images", force: true } { action: "image", subaction: "pull", host: "tootie", image: "nginx:latest" } { action: "scout", subaction: "read", host: "tootie", path: "/etc/hosts" } { action: "scout", subaction: "list", host: "tootie", path: "/var/log" } { action: "scout", subaction: "exec", host: "tootie", path: "/tmp", command: "ls -la" }

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • MCP tool registration for 'homelab' tool, including schema reference, annotations, and handler function that validates input and routes to action handlers.
    server.registerTool( "homelab", { title: "Homelab Manager", description: TOOL_DESCRIPTION, inputSchema: UnifiedHomelabSchema, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true } }, async (params: unknown) => { try { // Validate and parse input with Zod const validated = UnifiedHomelabSchema.parse(params); return await routeAction(validated, hosts, container); } catch (error) { return { isError: true, content: [ { type: "text" as const, text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` } ] }; } } );
  • Zod schema definition for the homelab tool using discriminated union on 'action_subaction' for efficient validation across all container, compose, host, docker, image, and scout operations.
    // ===== Unified schema using z.discriminatedUnion for O(1) lookup ===== // Uses action_subaction composite key as discriminator for constant-time schema lookup // instead of O(n) sequential validation with z.union() const UnifiedHomelabUnion = z.discriminatedUnion("action_subaction", [ // Container actions (12 schemas) containerListSchema, containerStartSchema, containerStopSchema, containerRestartSchema, containerPauseSchema, containerUnpauseSchema, containerLogsSchema, containerStatsSchema, containerInspectSchema, containerSearchSchema, containerPullSchema, containerRecreateSchema, // Compose actions (9 schemas) composeListSchema, composeStatusSchema, composeUpSchema, composeDownSchema, composeRestartSchema, composeLogsSchema, composeBuildSchema, composeRecreateSchema, composePullSchema, // Host actions (2 schemas) hostStatusSchema, hostResourcesSchema, // Docker actions (3 schemas) dockerInfoSchema, dockerDfSchema, dockerPruneSchema, // Image actions (4 schemas) imageListSchema, imagePullSchema, imageBuildSchema, imageRemoveSchema, // Scout actions (7 schemas) scoutReadSchema, scoutListSchema, scoutTreeSchema, scoutExecSchema, scoutFindSchema, scoutTransferSchema, scoutDiffSchema ]); // Export with preprocess wrapper to automatically inject discriminator export const UnifiedHomelabSchema = z.preprocess(preprocessWithDiscriminator, UnifiedHomelabUnion); export type UnifiedHomelabInput = z.infer<typeof UnifiedHomelabSchema>;
  • Central router function that dispatches validated input to specific action handlers (container, compose, host, docker, image, scout).
    async function routeAction( params: UnifiedHomelabInput, hosts: HostConfig[], container: ServiceContainer ): Promise<{ isError?: boolean; content: Array<{ type: "text"; text: string }>; structuredContent?: Record<string, unknown>; }> { const { action } = params; switch (action) { case "container": return handleContainerAction(params, hosts, container); case "compose": return handleComposeAction(params, hosts, container); case "host": return handleHostAction(params, hosts, container); case "docker": return handleDockerAction(params, hosts, container); case "image": return handleImageAction(params, hosts, container); case "scout": return handleScoutAction(params, hosts, container); default: throw new Error(`Unknown action: ${action}`); } }
  • Handler for container operations: list, start/stop/restart/pause/unpause, logs, stats, inspect, search, pull image, recreate. Supports multi-host, pagination, filtering, and parallel stats collection.
    async function handleContainerAction( params: UnifiedHomelabInput, hosts: HostConfig[], container: ServiceContainer ): Promise<{ isError?: boolean; content: Array<{ type: "text"; text: string }>; structuredContent?: Record<string, unknown>; }> { if (params.action !== "container") throw new Error("Invalid action"); const { subaction } = params; const dockerService = container.getDockerService(); switch (subaction) { case "list": { const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts; if (params.host && targetHosts.length === 0) { return errorResponse( `Host '${params.host}' not found. Available: ${hosts.map((h) => h.name).join(", ")}` ); } const containers = await dockerService.listContainers(targetHosts, { state: params.state, nameFilter: params.name_filter, imageFilter: params.image_filter, labelFilter: params.label_filter }); const total = containers.length; const paginated = containers.slice(params.offset, params.offset + params.limit); const hasMore = total > params.offset + params.limit; const output = { total, count: paginated.length, offset: params.offset, containers: paginated, has_more: hasMore }; const text = params.response_format === ResponseFormat.JSON ? JSON.stringify(output, null, 2) : formatContainersMarkdown(paginated, total, params.offset, hasMore); return successResponse(text, output); } case "start": case "stop": case "restart": case "pause": case "unpause": { const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService); if (!targetHost) { return errorResponse(`Container '${params.container_id}' not found.`); } await dockerService.containerAction(params.container_id, subaction, targetHost); return successResponse( `✓ Successfully performed '${subaction}' on container '${params.container_id}' (host: ${targetHost.name})` ); } case "logs": { const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService); if (!targetHost) { return errorResponse(`Container '${params.container_id}' not found.`); } let logs = await dockerService.getContainerLogs(params.container_id, targetHost, { lines: params.lines, since: params.since, until: params.until, stream: params.stream }); if (params.grep) { const grepLower = params.grep.toLowerCase(); logs = logs.filter((l) => l.message.toLowerCase().includes(grepLower)); } const output = { container: params.container_id, host: targetHost.name, count: logs.length, logs }; const text = params.response_format === ResponseFormat.JSON ? JSON.stringify(output, null, 2) : formatLogsMarkdown(logs, params.container_id, targetHost.name); return successResponse(text, output); } case "stats": { if (params.container_id) { const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService); if (!targetHost) { return errorResponse(`Container '${params.container_id}' not found.`); } const stats = await dockerService.getContainerStats(params.container_id, targetHost); const output = { ...stats, host: targetHost.name }; const text = params.response_format === ResponseFormat.JSON ? JSON.stringify(output, null, 2) : formatStatsMarkdown([stats], targetHost.name); return successResponse(text, output); } else { const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts; // Collect stats in parallel across all hosts and containers const allStats = await collectStatsParallel(targetHosts, dockerService, 20); const output = { stats: allStats.map((s) => ({ ...s.stats, host: s.host })) }; const text = params.response_format === ResponseFormat.JSON ? JSON.stringify(output, null, 2) : formatMultiStatsMarkdown(allStats); return successResponse(text, output); } } case "inspect": { const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService); if (!targetHost) { return errorResponse(`Container '${params.container_id}' not found.`); } const info = await dockerService.inspectContainer(params.container_id, targetHost); // Summary mode returns condensed output to save tokens if (params.summary) { const summary = { id: info.Id?.slice(0, 12), name: info.Name?.replace(/^\//, ""), image: info.Config?.Image, state: info.State?.Status, created: info.Created, started: info.State?.StartedAt, restartCount: info.RestartCount, ports: Object.keys(info.NetworkSettings?.Ports || {}).filter( (p) => info.NetworkSettings?.Ports?.[p] ), mounts: (info.Mounts || []).map( (m: { Source?: string; Destination?: string; Type?: string }) => ({ src: m.Source, dst: m.Destination, type: m.Type }) ), networks: Object.keys(info.NetworkSettings?.Networks || {}), env_count: (info.Config?.Env || []).length, labels_count: Object.keys(info.Config?.Labels || {}).length, host: targetHost.name }; const text = params.response_format === ResponseFormat.JSON ? JSON.stringify(summary, null, 2) : formatInspectSummaryMarkdown(summary); return successResponse(text, summary); } // Full mode returns complete inspect output const output = { ...info, _host: targetHost.name }; const text = params.response_format === ResponseFormat.JSON ? JSON.stringify(output, null, 2) : formatInspectMarkdown(info, targetHost.name); return successResponse(text, output); } case "search": { const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts; const allContainers = await dockerService.listContainers(targetHosts, {}); const query = params.query.toLowerCase(); const matches = allContainers.filter((c) => { const searchText = [c.name, c.image, ...Object.keys(c.labels), ...Object.values(c.labels)] .join(" ") .toLowerCase(); return searchText.includes(query); }); const total = matches.length; const paginated = matches.slice(params.offset, params.offset + params.limit); const hasMore = total > params.offset + params.limit; const output = { query: params.query, total, count: paginated.length, containers: paginated, has_more: hasMore }; const text = params.response_format === ResponseFormat.JSON ? JSON.stringify(output, null, 2) : formatSearchResultsMarkdown(paginated, params.query, total); return successResponse(text, output); } case "pull": { const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService); if (!targetHost) { return errorResponse(`Container '${params.container_id}' not found.`); } const info = await dockerService.inspectContainer(params.container_id, targetHost); const imageName = info.Config.Image; await dockerService.pullImage(imageName, targetHost); return successResponse( `✓ Successfully pulled latest image '${imageName}' for container '${params.container_id}'` ); } case "recreate": { const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService); if (!targetHost) { return errorResponse(`Container '${params.container_id}' not found.`); } const result = await dockerService.recreateContainer(params.container_id, targetHost, { pull: params.pull }); return successResponse( `✓ ${result.status}. New container ID: ${result.containerId.slice(0, 12)}` ); } default: throw new Error(`Unknown container subaction: ${subaction}`); } }
  • Top-level tool registration entrypoint that invokes unified homelab tool registration.
    export function registerTools(server: McpServer, container?: ServiceContainer): void { if (!container) { throw new Error("ServiceContainer is required for tool registration"); } registerUnifiedTool(server, container); }

Other Tools

Latest Blog Posts

MCP directory API

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